| 502 | }; |
| 503 | |
| 504 | export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecutionError>( |
| 505 | config: ExecutionEngineConfig<E>, |
| 506 | ): ExecutionEngine<E> => { |
| 507 | const { executor, codeExecutor, toolDiscoveryProvider = defaultToolDiscoveryProvider } = config; |
| 508 | const pausedExecutions = new Map<string, InternalPausedExecution<E>>(); |
| 509 | // Every sandbox fiber `startPausableExecution` still has in flight. |
| 510 | // |
| 511 | // Those fibers are daemons (`Effect.forkDetach`) so a pause can outlive the |
| 512 | // caller that observed it. But they close over `executor`, and the executor |
| 513 | // closes over the FumaDB handle the host opened for whatever scope built THIS |
| 514 | // engine — `makeFumaClient` captures `db` at construction, not per operation. |
| 515 | // A host that builds one engine per HTTP request therefore needs a way to end |
| 516 | // that fiber's life with the request; otherwise it wakes up after the |
| 517 | // request's postgres pool has been closed and every query it makes lands on a |
| 518 | // dead pool. `shutdown` below is that seam. |
| 519 | const liveSandboxFibers = new Set<Fiber.Fiber<ExecuteResult, E>>(); |
| 520 | // Outcomes of executions that already settled (resumed to completion, hit a |
| 521 | // new pause, or died while paused). MCP clients retry `resume` when a |
| 522 | // response gets lost in transit; without this cache the retry of an |
| 523 | // already-delivered resume answers "no paused execution" (observed in |
| 524 | // production seconds after a successful resume). Bounded FIFO — pause |
| 525 | // volume is tiny (human approvals), so a small window is plenty. |
| 526 | const settledOutcomes = new Map<string, Exit.Exit<ExecutionResult, E>>(); |
| 527 | const SETTLED_OUTCOME_LIMIT = 64; |
| 528 | const settledExecutionIds = new Set<string>(); |
| 529 | const SETTLED_EXECUTION_ID_LIMIT = 1024; |
| 530 | // Resumes whose outcome is still being computed, so a concurrent duplicate |
| 531 | // awaits the same result instead of missing the (already-consumed) pause. |
| 532 | const pendingResumes = new Map<string, Deferred.Deferred<ExecutionResult, E>>(); |
| 533 | |
| 534 | // Exits (not just successes) so a replayed failure re-fails through the |
| 535 | // typed channel — hosts render engine failures opaquely, and a replay must |
| 536 | // not bypass that by flattening the cause into result text. |
| 537 | const recordSettledOutcome = (executionId: string, exit: Exit.Exit<ExecutionResult, E>): void => { |
| 538 | settledExecutionIds.add(executionId); |
| 539 | while (settledExecutionIds.size > SETTLED_EXECUTION_ID_LIMIT) { |
| 540 | const oldest = settledExecutionIds.keys().next().value; |
| 541 | if (oldest === undefined) break; |
| 542 | settledExecutionIds.delete(oldest); |
| 543 | } |
| 544 | settledOutcomes.set(executionId, exit); |
| 545 | while (settledOutcomes.size > SETTLED_OUTCOME_LIMIT) { |
| 546 | const oldest = settledOutcomes.keys().next().value; |
| 547 | if (oldest === undefined) break; |
| 548 | settledOutcomes.delete(oldest); |
| 549 | } |
| 550 | }; |
| 551 | |
| 552 | /** |
| 553 | * Race a running fiber against the pause queue. Returns when either |
| 554 | * the fiber completes or an elicitation handler fires (whichever |
| 555 | * comes first). Re-used by both executeWithPause and resume. |
| 556 | * |
| 557 | * `Effect.raceFirst` (not `Effect.race`) — `race` has prefer-success |
| 558 | * semantics in Effect v4 ("first successful result"), which means a |
| 559 | * fiber failure waits indefinitely for the pause Deferred to succeed. |
| 560 | * For a fast `codeExecutor.execute` failure (e.g. a syntax error |
| 561 | * inside the dynamic worker) the pause signal never fires, so the |