@opencode/codemode 2.1.0 → 2.1.2

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.
@@ -26,16 +26,19 @@ export const executeProgram = (code, prepared, limits, hooks, globals) => {
26
26
  const program = parseProgram(code);
27
27
  const pending = new Pending(scope, builtins.Promise);
28
28
  const ctx = new Interpreter({ tools, pending, builtins, logs, globals });
29
- const result = (yield* toBoundary(ctx, yield* ctx.run(program))) ?? null;
30
- returned = { value: result, pending };
31
- const warnings = yield* pending.interrupt();
32
- return {
33
- ok: true,
34
- value: result,
35
- ...(warnings.length > 0 ? { warnings } : {}),
36
- ...logged(),
37
- toolCalls: tools.calls,
38
- };
29
+ // The main job holds the turn until leftover promise work is interrupted.
30
+ return yield* pending.turn.hold(Effect.gen(function* () {
31
+ const result = (yield* toBoundary(ctx, yield* ctx.run(program))) ?? null;
32
+ returned = { value: result, pending };
33
+ const warnings = yield* pending.interrupt();
34
+ return {
35
+ ok: true,
36
+ value: result,
37
+ ...(warnings.length > 0 ? { warnings } : {}),
38
+ ...logged(),
39
+ toolCalls: tools.calls,
40
+ };
41
+ }));
39
42
  }), (scope, exit) => Scope.close(scope, exit));
40
43
  const timeoutMs = limits.timeoutMs;
41
44
  const operation = timeoutMs === undefined
@@ -140,7 +140,7 @@ export const extensionGlobals = (ctx, extensions) => {
140
140
  }
141
141
  if (!(result instanceof Promise))
142
142
  return settle(Effect.succeed(result));
143
- return ctx.pending.create(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason })));
143
+ return ctx.pending.createHost(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason })));
144
144
  });
145
145
  return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name, (args) => ({ extension: extension.name, name, args }))]));
146
146
  };
@@ -286,18 +286,24 @@ class Frame {
286
286
  return value;
287
287
  }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())));
288
288
  }
289
- // Fork at the call site so admission and hooks occur when the call is made.
289
+ // Fork at the call site so admission and hooks occur when the call is made. Arguments cross on the caller's turn,
290
+ // since `toJSON` is program code; the host call runs without it, so tool calls stay concurrent.
290
291
  createToolCallPromise(path, args) {
291
- return this.ctx.pending.create(this.ctx.tool((json) => this.ctx.tools.execute(path, json), args));
292
+ const pending = this.ctx.pending;
293
+ return pending.create(this.ctx.tool((json) => pending.turn.leave(this.ctx.tools.execute(path, json)), args));
292
294
  }
293
295
  // Fiber exits make settlement idempotent; yielding prevents inline continuation.
294
296
  await(promise) {
295
297
  const pending = this.ctx.pending;
296
298
  return Effect.suspend(() => {
297
299
  pending.markObserved(promise);
298
- return Effect.flatMap(pending.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit));
300
+ return Effect.flatten(pending.turn.suspend(Effect.flatMap(pending.await(promise), (exit) => Effect.as(Effect.yieldNow, exit))));
299
301
  });
300
302
  }
303
+ // End this job; the program continues in a later one, after the jobs already waiting.
304
+ nextJob() {
305
+ return this.ctx.pending.turn.suspend(Effect.yieldNow);
306
+ }
301
307
  evaluateStatement(node) {
302
308
  switch (node.type) {
303
309
  case "ExpressionStatement":
@@ -549,7 +555,7 @@ class Frame {
549
555
  const close = () => iterator
550
556
  ? self.closeIterator(iterator, node, awaiting)
551
557
  : awaiting
552
- ? Effect.andThen(cursor?.close ?? Effect.void, Effect.yieldNow)
558
+ ? Effect.andThen(cursor?.close ?? Effect.void, self.nextJob())
553
559
  : (cursor?.close ?? Effect.void);
554
560
  if (left.type === "RestElement" || left.type === "AssignmentPattern") {
555
561
  throw typeError("Unsupported for...of binding.", left);
@@ -675,7 +681,7 @@ class Frame {
675
681
  const called = yield* Effect.exit(self.call(iterator.next, iterator.iterator, [], node));
676
682
  if (!Exit.isSuccess(called)) {
677
683
  if (awaiting)
678
- yield* Effect.yieldNow;
684
+ yield* self.nextJob();
679
685
  return yield* Effect.failCause(called.cause);
680
686
  }
681
687
  const captured = yield* Effect.exit(Effect.sync(() => {
@@ -684,7 +690,7 @@ class Frame {
684
690
  }));
685
691
  if (!Exit.isSuccess(captured)) {
686
692
  if (awaiting)
687
- yield* Effect.yieldNow;
693
+ yield* self.nextJob();
688
694
  return yield* Effect.failCause(captured.cause);
689
695
  }
690
696
  return {
@@ -697,8 +703,9 @@ class Frame {
697
703
  }
698
704
  closeIterator(iterator, node, awaiting = true) {
699
705
  const close = get(iterator.iterator, "return");
700
- if (close === undefined || close === null)
701
- return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow;
706
+ if (close === undefined || close === null) {
707
+ return iterator.asynchronous || !awaiting ? Effect.void : this.nextJob();
708
+ }
702
709
  const self = this;
703
710
  return Effect.gen(function* () {
704
711
  const method = self.requireIteratorMethod(close, "Iterator return", node);
@@ -709,13 +716,13 @@ class Frame {
709
716
  const called = yield* Effect.exit(self.call(method, iterator.iterator, [], node));
710
717
  if (!Exit.isSuccess(called)) {
711
718
  if (awaiting)
712
- yield* Effect.yieldNow;
719
+ yield* self.nextJob();
713
720
  return yield* Effect.failCause(called.cause);
714
721
  }
715
722
  const captured = yield* Effect.exit(Effect.sync(() => get(self.requireIteratorObject(called.value, "Iterator return() result", node), "value")));
716
723
  if (!Exit.isSuccess(captured)) {
717
724
  if (awaiting)
718
- yield* Effect.yieldNow;
725
+ yield* self.nextJob();
719
726
  return yield* Effect.failCause(captured.cause);
720
727
  }
721
728
  if (awaiting)
@@ -1506,8 +1513,14 @@ class Frame {
1506
1513
  invocation.generatorAsync = asynchronous;
1507
1514
  const builtins = this.ctx.builtins;
1508
1515
  const result = (value, done) => record(builtins.Object, { value, done });
1516
+ const turn = this.ctx.pending.turn;
1509
1517
  const request = (kind, value) => {
1510
1518
  const request = { kind, value, response: Deferred.makeUnsafe() };
1519
+ // A body that ran on the caller's turn has answered or suspended when it hands the turn back. An answer still
1520
+ // pending can only be an async generator's: its promise settles when the body answers, without the turn.
1521
+ const answer = Effect.suspend(() => Deferred.isDoneUnsafe(request.response)
1522
+ ? Deferred.await(request.response)
1523
+ : turn.leave(Deferred.await(request.response)));
1511
1524
  if (!asynchronous && state.active)
1512
1525
  return Effect.die(typeError("Generator is already running."));
1513
1526
  if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
@@ -1515,11 +1528,11 @@ class Frame {
1515
1528
  state.completed = true;
1516
1529
  state.pending.push(request);
1517
1530
  if (state.draining)
1518
- return Deferred.await(request.response);
1531
+ return answer;
1519
1532
  state.draining = true;
1520
1533
  return Effect.andThen(this.ctx.pending.fork(invocation
1521
1534
  .completeGeneratorRequests(state, true)
1522
- .pipe(Effect.ensuring(Effect.sync(() => (state.draining = false))))), Deferred.await(request.response));
1535
+ .pipe(Effect.ensuring(Effect.sync(() => (state.draining = false))))), answer);
1523
1536
  }
1524
1537
  if (state.completed) {
1525
1538
  if (kind === "throw")
@@ -1534,9 +1547,10 @@ class Frame {
1534
1547
  }
1535
1548
  state.pending.push(request);
1536
1549
  if (state.available) {
1550
+ // The body is parked waiting for a request: it runs this step on the caller's turn.
1537
1551
  const available = state.available;
1538
1552
  state.available = undefined;
1539
- Deferred.doneUnsafe(available, Exit.succeed(undefined));
1553
+ return Effect.andThen(turn.handOff(Effect.sync(() => Deferred.doneUnsafe(available, Exit.succeed(undefined)))), answer);
1540
1554
  }
1541
1555
  if (!state.started) {
1542
1556
  state.started = true;
@@ -1555,9 +1569,9 @@ class Frame {
1555
1569
  yield* invocation.completeGeneratorRequests(state, asynchronous);
1556
1570
  state.completed = true;
1557
1571
  });
1558
- return Effect.andThen(this.ctx.pending.fork(body), Deferred.await(request.response));
1572
+ return Effect.andThen(this.ctx.pending.fork(body), answer);
1559
1573
  }
1560
- return Deferred.await(request.response);
1574
+ return answer;
1561
1575
  };
1562
1576
  const proto = get(fn, "prototype");
1563
1577
  return new GeneratorObj(proto instanceof Obj ? proto : builtins.Generator, asynchronous, request);
@@ -1588,7 +1602,7 @@ class Frame {
1588
1602
  if (next)
1589
1603
  return Effect.succeed(next);
1590
1604
  state.available = Deferred.makeUnsafe();
1591
- return Effect.andThen(Deferred.await(state.available), Effect.sync(() => this.dequeueGeneratorRequest(state)));
1605
+ return Effect.andThen(this.ctx.pending.turn.park(Deferred.await(state.available)), Effect.sync(() => this.dequeueGeneratorRequest(state)));
1592
1606
  }
1593
1607
  dequeueGeneratorRequest(state) {
1594
1608
  const request = state.pending[state.pendingIndex];
@@ -1,10 +1,36 @@
1
- import { Effect, Exit, Scope } from "effect";
1
+ import { Effect, Exit, Fiber, Scope } from "effect";
2
2
  import type { Diagnostic } from "../codemode.js";
3
3
  import { Obj, PromiseObj, type Value } from "./objects.js";
4
4
  import type { Interpreter } from "./interpreter.js";
5
+ /**
6
+ * The right to run program code. JavaScript runs one job at a time: code between two awaits finishes before another
7
+ * job starts. Effect preempts a busy fiber so timeouts and host work still run, so only the fiber holding the turn may
8
+ * run program code. A synchronous call into another fiber (an async function's body up to its first await, a
9
+ * generator step) lends that fiber the turn and gets it back when the callee suspends or ends. An await gives the turn
10
+ * to the next waiting job and queues to take it back. Work that only settles a promise or runs host code leaves
11
+ * without it, so tool calls stay concurrent and resolvers settle in the call, as in JavaScript.
12
+ */
13
+ export declare class Turn {
14
+ private holder;
15
+ private readonly handbacks;
16
+ private first;
17
+ private last;
18
+ private nesting;
19
+ readonly take: Effect.Effect<void>;
20
+ hold<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
21
+ release(fiber: Fiber.Fiber<unknown, unknown>): void;
22
+ suspend<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
23
+ leave<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
24
+ handOff<A, E, R>(start: Effect.Effect<A, E, R>): Effect.Effect<void, E, R>;
25
+ park<A, E, R>(wait: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
26
+ fork<A, E, R>(effect: Effect.Effect<A, E, R>, scope: Scope.Scope): Effect.Effect<Fiber.Fiber<A, E>, never, R>;
27
+ private handBack;
28
+ private nest;
29
+ }
5
30
  export declare class Pending<R> {
6
31
  private readonly scope;
7
32
  private readonly proto;
33
+ readonly turn: Turn;
8
34
  private readonly active;
9
35
  private readonly ids;
10
36
  private readonly observed;
@@ -15,6 +41,8 @@ export declare class Pending<R> {
15
41
  promise?: PromiseObj;
16
42
  }) => Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
17
43
  create(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
44
+ createHost(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R>;
45
+ private register;
18
46
  markObserved(promise: PromiseObj): void;
19
47
  await(promise: PromiseObj): Effect.Effect<Exit.Exit<Value, unknown>>;
20
48
  fork(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<void, never, R>;
@@ -11,10 +11,165 @@ const capability = (ctx, name, settle) => fn(ctx.builtins, name, 1, (_, args) =>
11
11
  settle(args[0]);
12
12
  return undefined;
13
13
  });
14
+ // Program fibers that synchronous starts and handbacks may nest on the host stack. Deeper ones start and resume
15
+ // through the scheduler, so deep recursion through async calls cannot overflow the host stack.
16
+ const MAX_NESTING = 100;
17
+ /**
18
+ * The right to run program code. JavaScript runs one job at a time: code between two awaits finishes before another
19
+ * job starts. Effect preempts a busy fiber so timeouts and host work still run, so only the fiber holding the turn may
20
+ * run program code. A synchronous call into another fiber (an async function's body up to its first await, a
21
+ * generator step) lends that fiber the turn and gets it back when the callee suspends or ends. An await gives the turn
22
+ * to the next waiting job and queues to take it back. Work that only settles a promise or runs host code leaves
23
+ * without it, so tool calls stay concurrent and resolvers settle in the call, as in JavaScript.
24
+ */
25
+ export class Turn {
26
+ holder;
27
+ // Callers waiting for a callee to hand the turn back, innermost last.
28
+ handbacks = [];
29
+ // Jobs waiting to take the turn, in the order they became ready.
30
+ first;
31
+ last;
32
+ nesting = 0;
33
+ // Take the turn when it is free, after the jobs already waiting.
34
+ take = Effect.withFiber((fiber) => {
35
+ if (this.holder === undefined) {
36
+ this.holder = fiber;
37
+ return Effect.void;
38
+ }
39
+ return Effect.callback((resume) => {
40
+ const waiter = { fiber, resume, removed: false };
41
+ if (this.last === undefined)
42
+ this.first = waiter;
43
+ else
44
+ this.last.next = waiter;
45
+ this.last = waiter;
46
+ // Interrupted while waiting, or after the turn was granted but before it ran.
47
+ return Effect.sync(() => {
48
+ if (this.holder === fiber)
49
+ this.release(fiber);
50
+ else
51
+ waiter.removed = true;
52
+ });
53
+ });
54
+ });
55
+ // Run `effect` holding the turn, as the program's main job does.
56
+ hold(effect) {
57
+ return Effect.withFiber((fiber) => Effect.andThen(this.take, effect).pipe(Effect.ensuring(Effect.sync(() => this.release(fiber)))));
58
+ }
59
+ // Give up the turn, back to the innermost caller waiting for it or to the next waiting job.
60
+ release(fiber) {
61
+ if (this.holder !== fiber)
62
+ return;
63
+ const back = this.handbacks.pop();
64
+ if (back !== undefined) {
65
+ this.holder = back.fiber;
66
+ this.handBack(back);
67
+ return;
68
+ }
69
+ let next = this.first;
70
+ while (next !== undefined && next.removed)
71
+ next = next.next;
72
+ this.first = next?.next;
73
+ if (this.first === undefined)
74
+ this.last = undefined;
75
+ this.holder = next?.fiber;
76
+ // Resume in the scheduler, as a yield would: release can run inside another fiber's step.
77
+ if (next !== undefined)
78
+ next.fiber.currentDispatcher.scheduleTask(() => next.resume(Effect.void), 0);
79
+ }
80
+ // Wait without the turn, then queue for it like any job that became ready. An interrupted fiber stops without it.
81
+ suspend(wait) {
82
+ return Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
83
+ this.release(fiber);
84
+ return Effect.flatMap(Effect.exit(restore(wait)), (exit) => Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) ? exit : Effect.andThen(restore(this.take), exit));
85
+ }));
86
+ }
87
+ // Wait without the turn and finish without it, for work that only settles a promise or runs host code.
88
+ leave(wait) {
89
+ return Effect.withFiber((fiber) => {
90
+ this.release(fiber);
91
+ return wait;
92
+ });
93
+ }
94
+ // Run `start`, which wakes a fiber parked with `park`, and continue once it hands the turn back.
95
+ handOff(start) {
96
+ return Effect.withFiber((fiber) => {
97
+ const back = { fiber, returned: false };
98
+ this.handbacks.push(back);
99
+ return Effect.andThen(this.nest(start), awaitHandback(back));
100
+ });
101
+ }
102
+ // Wait without the turn for a caller to hand it over with `handOff`.
103
+ park(wait) {
104
+ return Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
105
+ this.release(fiber);
106
+ // The wake runs inside the caller's step; when that is deep, yield so the body continues on its own stack.
107
+ return Effect.tap(restore(wait), () => {
108
+ this.holder = fiber;
109
+ return this.nesting > MAX_NESTING ? Effect.yieldNow : Effect.void;
110
+ });
111
+ }));
112
+ }
113
+ // Fork a fiber that runs program code. It starts with the caller's turn, and the caller continues once the fiber
114
+ // suspends or ends; the fiber gives up the turn whenever it ends.
115
+ fork(effect, scope) {
116
+ return Effect.withFiber((caller) => {
117
+ const back = { fiber: caller, returned: false };
118
+ this.handbacks.push(back);
119
+ let started = false;
120
+ const body = Effect.withFiber((fiber) => {
121
+ started = true;
122
+ this.holder = fiber;
123
+ return effect;
124
+ });
125
+ const forked = this.nesting < MAX_NESTING
126
+ ? this.nest(Effect.forkIn(body, scope, { startImmediately: true }))
127
+ : Effect.forkIn(body, scope);
128
+ return Effect.flatMap(forked, (fiber) => {
129
+ fiber.addObserver(() => {
130
+ if (started)
131
+ return this.release(fiber);
132
+ // Forked into a closing scope: it never took the turn, so the caller keeps it.
133
+ const index = this.handbacks.lastIndexOf(back);
134
+ if (index >= 0)
135
+ this.handbacks.splice(index, 1);
136
+ this.handBack(back);
137
+ });
138
+ // A callee that suspends without blocking hands the turn back before `forkIn` returns.
139
+ return back.returned ? Effect.succeed(fiber) : Effect.as(awaitHandback(back), fiber);
140
+ });
141
+ });
142
+ }
143
+ // Resume a parked caller in the scheduler: resumed inline, it would run to its next await and queue before the callee
144
+ // that just suspended has queued its own continuation.
145
+ handBack(back) {
146
+ back.returned = true;
147
+ const resume = back.resume;
148
+ if (resume !== undefined)
149
+ back.fiber.currentDispatcher.scheduleTask(() => resume(Effect.void), 0);
150
+ }
151
+ // Run `effect`, which can run another program fiber inside the current step, counting the nesting.
152
+ nest(effect) {
153
+ return Effect.suspend(() => {
154
+ this.nesting++;
155
+ return Effect.ensuring(effect, Effect.sync(() => {
156
+ this.nesting--;
157
+ }));
158
+ });
159
+ }
160
+ }
161
+ // The check runs when the caller suspends: it can be preempted after deciding to wait.
162
+ const awaitHandback = (back) => Effect.callback((resume) => {
163
+ if (back.returned)
164
+ resume(Effect.void);
165
+ else
166
+ back.resume = resume;
167
+ });
14
168
  // Observation only controls rejection reporting; program completion interrupts all promise work.
15
169
  export class Pending {
16
170
  scope;
17
171
  proto;
172
+ turn = new Turn();
18
173
  active = new Set();
19
174
  ids = new WeakMap();
20
175
  observed = new WeakSet();
@@ -32,7 +187,15 @@ export class Pending {
32
187
  return promise;
33
188
  });
34
189
  }
190
+ // A promise whose work runs program code, on the turn. Host work that runs no program code uses `createHost`.
35
191
  create(effect) {
192
+ return this.register(effect, (body) => this.turn.fork(body, this.scope));
193
+ }
194
+ // Starts now, without the turn, so host work stays concurrent and attaches to host promises at once.
195
+ createHost(effect) {
196
+ return this.register(effect, (body) => Effect.forkIn(body, this.scope, { startImmediately: true }));
197
+ }
198
+ register(effect, fork) {
36
199
  return Effect.flatMap(CallSite, (site) => {
37
200
  if (this.active.size >= MAX_PENDING_PROMISES) {
38
201
  throw rangeError(`Too many pending promises (limit ${MAX_PENDING_PROMISES}); await promises before creating more.`);
@@ -40,7 +203,7 @@ export class Pending {
40
203
  // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
41
204
  const id = this.nextID++;
42
205
  const body = Effect.catchDefect(effect, (defect) => Effect.die(locate(defect, site.node)));
43
- return Effect.map(Effect.forkIn(body, this.scope, { startImmediately: true }), (fiber) => {
206
+ return Effect.map(fork(body), (fiber) => {
44
207
  const promise = new PromiseObj(this.proto, fiber);
45
208
  this.active.add(promise);
46
209
  this.ids.set(promise, id);
@@ -72,7 +235,7 @@ export class Pending {
72
235
  return Fiber.await(promise.fiber);
73
236
  }
74
237
  fork(effect) {
75
- return Effect.asVoid(Effect.forkIn(effect, this.scope, { startImmediately: true }));
238
+ return Effect.asVoid(this.turn.fork(effect, this.scope));
76
239
  }
77
240
  diagnostics() {
78
241
  return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure);
@@ -101,7 +264,7 @@ export const resolvePromiseValue = (ctx, value, own) => {
101
264
  return Effect.succeed(value);
102
265
  return Effect.gen(function* () {
103
266
  // Promise resolution invokes a thenable's method in a later job.
104
- yield* Effect.yieldNow;
267
+ yield* ctx.pending.turn.suspend(Effect.yieldNow);
105
268
  const deferred = Deferred.makeUnsafe();
106
269
  const resolve = capability(ctx, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)));
107
270
  const reject = capability(ctx, "reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(reason))));
@@ -111,7 +274,7 @@ export const resolvePromiseValue = (ctx, value, own) => {
111
274
  return yield* Effect.failCause(executed.cause);
112
275
  Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)));
113
276
  }
114
- return yield* resolvePromiseValue(ctx, yield* Deferred.await(deferred), own);
277
+ return yield* resolvePromiseValue(ctx, yield* ctx.pending.turn.leave(Deferred.await(deferred)), own);
115
278
  });
116
279
  };
117
280
  export const resolvePromise = (ctx, value) => {
@@ -157,31 +320,33 @@ const invokePromiseMethod = (ctx, name, args) => {
157
320
  items.push(item);
158
321
  }
159
322
  if (name === "all") {
160
- return new Arr(ctx.builtins.Array, yield* settleAfterTurn(Effect.all(items.map((item) => Effect.flatten(ctx.pending.await(item))), { concurrency: "unbounded" })));
323
+ return new Arr(ctx.builtins.Array, yield* settleAfterTurn(ctx, Effect.all(items.map((item) => Effect.flatten(ctx.pending.await(item))), { concurrency: "unbounded" })));
161
324
  }
162
325
  if (name === "allSettled") {
163
- const outcomes = [];
164
- for (const item of items) {
165
- const exit = yield* ctx.pending.await(item);
166
- if (Exit.isSuccess(exit)) {
167
- outcomes.push(record(ctx.builtins.Object, { status: "fulfilled", value: exit.value }));
168
- continue;
326
+ return yield* ctx.pending.turn.suspend(Effect.gen(function* () {
327
+ const outcomes = [];
328
+ for (const item of items) {
329
+ const exit = yield* ctx.pending.await(item);
330
+ if (Exit.isSuccess(exit)) {
331
+ outcomes.push(record(ctx.builtins.Object, { status: "fulfilled", value: exit.value }));
332
+ continue;
333
+ }
334
+ if (Cause.hasInterruptsOnly(exit.cause))
335
+ return yield* Effect.failCause(exit.cause);
336
+ outcomes.push(record(ctx.builtins.Object, {
337
+ status: "rejected",
338
+ reason: materialize(ctx, Cause.squash(exit.cause)),
339
+ }));
169
340
  }
170
- if (Cause.hasInterruptsOnly(exit.cause))
171
- return yield* Effect.failCause(exit.cause);
172
- outcomes.push(record(ctx.builtins.Object, {
173
- status: "rejected",
174
- reason: materialize(ctx, Cause.squash(exit.cause)),
175
- }));
176
- }
177
- yield* Effect.yieldNow;
178
- return new Arr(ctx.builtins.Array, outcomes);
341
+ yield* Effect.yieldNow;
342
+ return new Arr(ctx.builtins.Array, outcomes);
343
+ }));
179
344
  }
180
345
  if (name === "race") {
181
346
  if (items.length === 0) {
182
347
  throw typeError("Promise.race([]) would never settle; provide at least one promise or value.");
183
348
  }
184
- return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => ctx.pending.await(item)))));
349
+ return yield* settleAfterTurn(ctx, Effect.flatten(Effect.raceAll(items.map((item) => ctx.pending.await(item)))));
185
350
  }
186
351
  const flipped = items.map((item) => Effect.flatMap(ctx.pending.await(item), (exit) => {
187
352
  if (Exit.isSuccess(exit))
@@ -190,7 +355,7 @@ const invokePromiseMethod = (ctx, name, args) => {
190
355
  return Effect.failCause(exit.cause);
191
356
  return Effect.succeed(materialize(ctx, Cause.squash(exit.cause)));
192
357
  }));
193
- return yield* settleAfterTurn(Effect.all(flipped, { concurrency: "unbounded" }).pipe(Effect.flatMap((reasons) => Effect.fail(new Throw(createAggregateErrorValue(ctx, reasons, "All promises were rejected")))), Effect.catch((error) => error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error))));
358
+ return yield* settleAfterTurn(ctx, Effect.all(flipped, { concurrency: "unbounded" }).pipe(Effect.flatMap((reasons) => Effect.fail(new Throw(createAggregateErrorValue(ctx, reasons, "All promises were rejected")))), Effect.catch((error) => error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error))));
194
359
  }));
195
360
  };
196
361
  const instanceMethod = (ctx, name, thisValue, args) => {
@@ -207,7 +372,7 @@ const instanceMethod = (ctx, name, thisValue, args) => {
207
372
  /** NewPromiseCapability: a pending promise with the resolve/reject callables that settle it exactly once. */
208
373
  const promiseCapability = (ctx) => Effect.gen(function* () {
209
374
  const deferred = Deferred.makeUnsafe();
210
- const promise = yield* ctx.pending.createWithSelf((self) => Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)));
375
+ const promise = yield* ctx.pending.createWithSelf((self) => Effect.flatMap(ctx.pending.turn.leave(Deferred.await(deferred)), (value) => resolvePromiseValue(ctx, value, self)));
211
376
  const resolve = capability(ctx, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)));
212
377
  const reject = capability(ctx, "reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(value))));
213
378
  return { promise, resolve, reject, deferred };
@@ -227,8 +392,9 @@ const constructPromise = (ctx, executor) => {
227
392
  return made.promise;
228
393
  });
229
394
  };
230
- // Settle one reaction turn after the deciding member, after its existing reactions.
231
- const settleAfterTurn = (body) => Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit));
395
+ // Settle one reaction turn after the deciding member, after its existing reactions. That reaction is a job: it queues
396
+ // for the turn behind the jobs ready before it and settles holding it.
397
+ const settleAfterTurn = (ctx, body) => Effect.flatten(ctx.pending.turn.suspend(Effect.flatMap(Effect.exit(body), (exit) => Effect.as(Effect.yieldNow, exit))));
232
398
  class PromiseAnyFulfilled {
233
399
  value;
234
400
  constructor(value) {
@@ -244,13 +410,13 @@ const reactionHandler = (value, method) => {
244
410
  return undefined;
245
411
  };
246
412
  // Teardown bypasses handlers; settled reactions yield once so handlers never run inline.
247
- const reactionExit = (ctx, source) => Effect.gen(function* () {
413
+ const reactionExit = (ctx, source) => ctx.pending.turn.suspend(Effect.gen(function* () {
248
414
  const exit = yield* ctx.pending.await(source);
249
415
  if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause))
250
416
  return yield* Effect.failCause(exit.cause);
251
417
  yield* Effect.yieldNow;
252
418
  return exit;
253
- });
419
+ }));
254
420
  const chainReaction = (ctx, source, onFulfilled, onRejected, method) => {
255
421
  return ctx.pending.createWithSelf((self) => Effect.gen(function* () {
256
422
  const exit = yield* reactionExit(ctx, source);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/codemode",
4
- "version": "2.1.0",
4
+ "version": "2.1.2",
5
5
  "description": "Effect-native confined code execution over schema-described tools",
6
6
  "type": "module",
7
7
  "license": "MIT",