{"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueAdapter","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#isSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#awaitResumedRunTerminal","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","#beginOptimistic","#settleOptimistic","AgentServerQueueAdapter","LocalQueueAdapter","#runAbort","bindThreadConfig","#scheduleQueueDrain"],"sources":["../../src/stream/submit-coordinator.ts"],"sourcesContent":["/**\n * Owns the run-submission lifecycle for a single\n * {@link StreamController}.\n *\n * # What this module is\n *\n * The {@link SubmitCoordinator} is the piece of the controller that\n * dispatches runs (`submit()`), enforces multitask strategies, queues\n * deferred submissions, races dispatch against terminal lifecycle\n * events, and surfaces errors back through the per-submit `onError`\n * callback and the root snapshot.\n *\n * Conceptually a submit looks like:\n *\n *   1. Optionally rebind to a different thread (`options.threadId`).\n *   2. Mint a thread id if one isn't bound yet.\n *   3. Wait for the controller's root pump to be ready (so the\n *      transport is subscribed before the run is dispatched —\n *      otherwise we could miss replayed events).\n *   4. Apply the {@link StreamSubmitOptions.multitaskStrategy} to\n *      decide whether to abort, enqueue, reject, or proceed.\n *   5. Race the dispatch promise (`thread.submitRun()`) against the next root\n *      terminal lifecycle event.\n *   6. Settle the resulting state (loading flag, error slot) and\n *      drain the next queued submission, if any.\n *\n * # Why it lives in its own class\n *\n * The submit lifecycle is the most state-heavy part of the\n * controller — six promises, an abort controller, a queue, a\n * terminal-vs-command race, and bidirectional callback wiring with\n * the controller. Splitting it out keeps `controller.ts` focused on\n * subscription / projection wiring while letting the submit logic\n * evolve independently.\n *\n * # Why we race \"command\" against \"terminal\"\n *\n * For fast runs, the server's terminal lifecycle event can arrive\n * *before* the dispatch HTTP response has resolved. Racing the two\n * lets us detect terminal early and not block waiting for a now-stale\n * dispatch response. The dispatch response is still consumed (via\n * `.then(notifyCreated).catch(reportError)`) so `onCreated` still\n * fires and dispatch errors still surface through `onError`.\n *\n * # Queue semantics (`multitaskStrategy: \"enqueue\"`)\n *\n * When a run is already in flight, an `\"enqueue\"` submit is delegated to\n * a {@link QueueAdapter} instead of being handled inline.\n * `SubmitCoordinator` only owns *which* adapter gets built and feeding\n * it a dispatch callback (`onIdle`) once the active run settles.\n *\n * @see StreamController - The owner; injects every collaborator dep.\n */\nimport { v7 as uuidv7 } from \"@langchain/core/utils/uuid\";\nimport type { ThreadStream } from \"../client/stream/index.js\";\nimport { bindThreadConfig } from \"./dispatch-config.js\";\nimport {\n  EMPTY_QUEUE,\n  type QueueAdapter,\n  type ServerQueueCapability,\n  type SubmissionQueueEntry,\n  type SubmissionQueueSnapshot,\n} from \"./queue-adapter.js\";\nimport { AgentServerQueueAdapter } from \"./queue-adapter-agent-server.js\";\nimport { LocalQueueAdapter } from \"./queue-adapter-local.js\";\nimport { StreamStore } from \"./store.js\";\nimport type { OptimisticHandle } from \"./optimistic-input.js\";\nimport type {\n  RootSnapshot,\n  RunExecutionReason,\n  StreamControllerOptions,\n  StreamSubmitOptions,\n} from \"./types.js\";\n\nexport { EMPTY_QUEUE };\nexport type { SubmissionQueueEntry, SubmissionQueueSnapshot };\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the three terminal lifecycle states the protocol surfaces, plus a\n * synthetic `\"aborted\"` for client-side cancellation.\n */\ntype TerminalResult = {\n  event: \"completed\" | \"failed\" | \"interrupted\" | \"aborted\";\n  error?: string;\n};\n\nfunction terminalReason(event: TerminalResult[\"event\"]): RunExecutionReason {\n  if (event === \"completed\") return \"success\";\n  if (event === \"failed\") return \"error\";\n  if (event === \"interrupted\") return \"interrupt\";\n  return \"stopped\";\n}\n\n/** Validates `client.runs`'s shape; throws rather than falling back to local. */\nfunction assertServerQueueCapability(\n  value: unknown\n): asserts value is ServerQueueCapability {\n  const ok =\n    typeof value === \"object\" &&\n    value != null &&\n    typeof (value as ServerQueueCapability).create === \"function\" &&\n    typeof (value as ServerQueueCapability).list === \"function\" &&\n    typeof (value as ServerQueueCapability).cancel === \"function\";\n  if (!ok) {\n    throw new Error(\n      'queue: \"server\" requires client.runs to implement create/list/cancel'\n    );\n  }\n}\n\n/**\n * Coordinates one controller's run-submission lifecycle.\n *\n * The constructor takes a bag of callbacks rather than a reference to\n * the parent {@link StreamController} on purpose:\n *\n *   - It keeps the dependency surface explicit and testable — every\n *     piece of controller state the submit lifecycle touches is one\n *     of these closures.\n *   - It avoids a cyclic dependency between controller and coordinator.\n *   - Tests can construct one with stub callbacks and assert behavior\n *     without mocking the entire controller.\n *\n * @typeParam StateType         - Root state shape.\n * @typeParam InterruptType     - Root interrupt payload shape.\n * @typeParam ConfigurableType  - `config.configurable` shape accepted\n *   by submit (usually `Record<string, unknown>`).\n */\nexport class SubmitCoordinator<\n  StateType extends object = Record<string, unknown>,\n  InterruptType = unknown,\n  ConfigurableType extends object = Record<string, unknown>,\n> {\n  /** Controller-level options forwarded into `submitRun` / callbacks. */\n  readonly #options: StreamControllerOptions<StateType>;\n  /** Root snapshot store; written for `isLoading`, `error`, `interrupts`. */\n  readonly #rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n  /** Pending submissions awaiting the active run to terminate. */\n  /** Backs `\"enqueue\"`: local-only defer, or real durable runs when `queue: \"server\"`. */\n  readonly #queueAdapter: QueueAdapter<StateType>;\n  /** Probes the controller's `disposed` flag from deferred work. */\n  readonly #getDisposed: () => boolean;\n  /** Reads the controller's currently-bound thread id. */\n  readonly #getCurrentThreadId: () => string | null;\n  /** Updates the controller's thread id (used when minting a new id). */\n  readonly #setCurrentThreadId: (threadId: string | null) => void;\n  /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */\n  readonly #rememberSelfCreatedThreadId: (threadId: string) => void;\n  /** True when the id is pending server-side create (minted or hydrate 404). */\n  readonly #isSelfCreatedThreadId: (threadId: string) => boolean;\n  /** Drops a thread id from the self-created set once it's committed server-side. */\n  readonly #forgetSelfCreatedThreadId: (threadId: string) => void;\n  /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */\n  readonly #hydrate: (threadId?: string | null) => Promise<void>;\n  /** Lazily creates / returns the active {@link ThreadStream}. */\n  readonly #ensureThread: (\n    threadId: string,\n    deferRootPump?: boolean\n  ) => ThreadStream;\n  /** Starts the previously-deferred root pump after a self-created thread commits. */\n  readonly #startDeferredRootPump: () => void;\n  /** Abandons a deferred root pump after a self-created dispatch fails. */\n  readonly #abandonDeferredRootPump: () => void;\n  /** Resolves once the controller's root subscription pump is up. */\n  readonly #waitForRootPumpReady: () => Promise<void> | undefined;\n  /** Resolves on the next root terminal lifecycle (or on abort). */\n  readonly #awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n  /**\n   * Resolves on the resumed run's terminal, skipping stale `interrupted`\n   * events from the run being resumed (see {@link dispatchResume}).\n   */\n  readonly #awaitResumedRunTerminal: (\n    signal: AbortSignal\n  ) => Promise<TerminalResult>;\n  /** Called once at the start of every {@link submit} invocation. */\n  readonly #onSubmitStart: () => void;\n  /** Marks that a local run dispatch is now active. */\n  readonly #onRunStart: () => void;\n  /** Records a server-accepted local run id and fires `onCreated`. */\n  readonly #onRunCreated: (runId: string) => void;\n  /** Fires `onCompleted` for the local run lifecycle. */\n  readonly #onRunCompleted: (\n    reason: RunExecutionReason,\n    runId?: string\n  ) => void;\n  /** Marks the local run dispatch lifecycle as settled. */\n  readonly #onRunEnd: () => void;\n  /**\n   * Apply a submit input optimistically before dispatch. Returns the\n   * id-injected payload to dispatch plus a handle for terminal\n   * reconciliation, or `undefined` when optimistic UI is disabled / no\n   * echo applies (dispatch the raw input).\n   */\n  readonly #beginOptimistic: (\n    input: unknown\n  ) => { dispatchInput: unknown; handle: OptimisticHandle } | undefined;\n  /** Reconcile optimistic state when a run terminates. */\n  readonly #settleOptimistic: (\n    handle: OptimisticHandle,\n    event: TerminalResult[\"event\"]\n  ) => void;\n\n  /**\n   * Active submission's abort controller. `undefined` between submits.\n   *\n   * Used both for `multitaskStrategy: \"rollback\"` (abort the previous\n   * controller's signal) and `stop()` (abort the current one without\n   * starting a new one).\n   */\n  #runAbort: AbortController | undefined;\n\n  constructor(params: {\n    options: StreamControllerOptions<StateType>;\n    rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n    queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n    getDisposed: () => boolean;\n    getCurrentThreadId: () => string | null;\n    setCurrentThreadId: (threadId: string | null) => void;\n    rememberSelfCreatedThreadId: (threadId: string) => void;\n    isSelfCreatedThreadId: (threadId: string) => boolean;\n    forgetSelfCreatedThreadId: (threadId: string) => void;\n    hydrate: (threadId?: string | null) => Promise<void>;\n    ensureThread: (threadId: string, deferRootPump?: boolean) => ThreadStream;\n    startDeferredRootPump: () => void;\n    abandonDeferredRootPump: () => void;\n    waitForRootPumpReady: () => Promise<void> | undefined;\n    awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n    awaitResumedRunTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n    onSubmitStart?: () => void;\n    onRunStart?: () => void;\n    onRunCreated?: (runId: string) => void;\n    onRunCompleted?: (reason: RunExecutionReason, runId?: string) => void;\n    onRunEnd?: () => void;\n    beginOptimistic?: (\n      input: unknown\n    ) => { dispatchInput: unknown; handle: OptimisticHandle } | undefined;\n    settleOptimistic?: (\n      handle: OptimisticHandle,\n      event: TerminalResult[\"event\"]\n    ) => void;\n  }) {\n    this.#options = params.options;\n    this.#rootStore = params.rootStore;\n    const onQueueError = (error: unknown) => {\n      this.#rootStore.setState((state) => ({ ...state, error }));\n    };\n    // Custom transports don't get server-backed queueing at all: there's\n    // nowhere on `AgentServerAdapter` to declare the capability. Chosen\n    // once, here, for this coordinator's lifetime and never re-evaluated.\n    const serverQueueEnabled =\n      typeof params.options.transport !== \"object\" &&\n      params.options.queue === \"server\";\n    if (serverQueueEnabled) {\n      assertServerQueueCapability(params.options.client.runs);\n    }\n    this.#queueAdapter = serverQueueEnabled\n      ? new AgentServerQueueAdapter(\n          params.options.client.runs,\n          params.options.assistantId,\n          params.queueStore,\n          onQueueError,\n          (threadId) => params.ensureThread(threadId, true) // deferRootPump: true, watching shouldn't force-start a root pump\n        )\n      : new LocalQueueAdapter(params.queueStore, onQueueError);\n    this.#getDisposed = params.getDisposed;\n    this.#getCurrentThreadId = params.getCurrentThreadId;\n    this.#setCurrentThreadId = params.setCurrentThreadId;\n    this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;\n    this.#isSelfCreatedThreadId = params.isSelfCreatedThreadId;\n    this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;\n    this.#hydrate = params.hydrate;\n    this.#ensureThread = params.ensureThread;\n    this.#startDeferredRootPump = params.startDeferredRootPump;\n    this.#abandonDeferredRootPump = params.abandonDeferredRootPump;\n    this.#waitForRootPumpReady = params.waitForRootPumpReady;\n    this.#awaitNextTerminal = params.awaitNextTerminal;\n    this.#awaitResumedRunTerminal = params.awaitResumedRunTerminal;\n    this.#onSubmitStart = params.onSubmitStart ?? (() => undefined);\n    this.#onRunStart = params.onRunStart ?? (() => undefined);\n    this.#onRunCreated = params.onRunCreated ?? (() => undefined);\n    this.#onRunCompleted = params.onRunCompleted ?? (() => undefined);\n    this.#onRunEnd = params.onRunEnd ?? (() => undefined);\n    this.#beginOptimistic = params.beginOptimistic ?? (() => undefined);\n    this.#settleOptimistic = params.settleOptimistic ?? (() => undefined);\n  }\n\n  /**\n   * Submit input to the active thread.\n   *\n   * Honours {@link StreamSubmitOptions.multitaskStrategy}:\n   *\n   *   - `\"rollback\"` (default): aborts any in-flight run and\n   *     dispatches immediately.\n   *   - `\"reject\"`: throws synchronously when a run is\n   *     already in flight.\n   *   - `\"enqueue\"`: defers via {@link QueueAdapter.enqueue};\n   *     resolves once accepted (durably, if server-backed), not on completion.\n   *   - `\"interrupt\"`: falls through to the default path\n   *\n   * Errors are routed through both the per-submit `onError` callback\n   * and `rootStore.error`. Aborts (controller dispose / rollback) are\n   * silently dropped.\n   *\n   * To resume a pending interrupt, use {@link StreamController.respond}\n   * instead of `submit()`.\n   *\n   * @param input   - Input payload for the run.\n   * @param options - Per-submit options (config, metadata, callbacks,\n   *   strategy, etc).\n   */\n  async submit(\n    input: unknown,\n    options?: StreamSubmitOptions<StateType, ConfigurableType>\n  ): Promise<void> {\n    if (this.#getDisposed()) return;\n\n    // Per-submit thread override: rebind first so the rest of the\n    // submit operates against the new thread.\n    const overrideThreadId = options?.threadId;\n    if (\n      overrideThreadId !== undefined &&\n      overrideThreadId !== this.#getCurrentThreadId()\n    ) {\n      await this.#hydrate(overrideThreadId);\n    }\n\n    // Self-created thread id path: mint client-side so the controller\n    // (and Suspense boundaries) get a stable id even before the run\n    // is dispatched.\n    const wasSelfCreated = this.#getCurrentThreadId() == null;\n    if (wasSelfCreated) {\n      const threadId = uuidv7();\n      this.#setCurrentThreadId(threadId);\n      this.#rememberSelfCreatedThreadId(threadId);\n      this.#options.onThreadId?.(threadId);\n      this.#rootStore.setState((s) => ({\n        ...s,\n        threadId,\n      }));\n    }\n\n    const currentThreadId = this.#getCurrentThreadId();\n    if (currentThreadId == null) return;\n    // For threads that don't exist server-side yet (just minted here,\n    // or an externally-minted id that hydrate marked missing via 404)\n    // we defer the persistent root SSE pump until after `submitRun` /\n    // `respondInput` commits the thread. Opening the pump's\n    // `subscription.subscribe` against a not-yet-existent thread row\n    // either 404s or — on langgraph_api's in-mem runtime — joins a\n    // dead subscription that delivers 0 bytes until idle reconnect.\n    // The deferred path starts the pump after dispatch returns (see\n    // `#startDeferredRootPump` calls below).\n    const pendingServerCreate =\n      wasSelfCreated || this.#isSelfCreatedThreadId(currentThreadId);\n    const thread = this.#ensureThread(currentThreadId, pendingServerCreate);\n    const activeThreadId = currentThreadId;\n\n    const strategy = options?.multitaskStrategy ?? \"rollback\";\n    // `wasSelfCreated` short-circuit: when this submit just minted a\n    // brand-new thread id (the user clicked \"New Thread\"), the\n    // strategy check shouldn't see a run on the *previous* thread as\n    // a reason to enqueue. The previous run is on a thread the user\n    // navigated away from; abandoning its client-side abort tracking\n    // is correct (the server-side run continues independently).\n    // Without this, `enqueue` would trap the new submission and\n    // `submitRun` never fires for the new thread — leaving a freshly-\n    // minted thread id committed to the URL but never to the server.\n    // True for a run this client dispatched (#runAbort) or one it only\n    // observed via the persistent lifecycle listener (started elsewhere —\n    // another tab, a webhook, Slack).\n    const hasActiveRun =\n      !wasSelfCreated &&\n      ((this.#runAbort != null && !this.#runAbort.signal.aborted) ||\n        this.#rootStore.getSnapshot().isLoading);\n    if (hasActiveRun && strategy === \"reject\") {\n      throw new Error(\n        \"submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.\"\n      );\n    }\n    if (hasActiveRun && strategy === \"enqueue\") {\n      try {\n        await this.#queueAdapter.enqueue(\n          currentThreadId,\n          (input ?? undefined) as Partial<StateType> | null | undefined,\n          // QueueAdapter is generic only over StateType, not ConfigurableType.\n          options as StreamSubmitOptions<StateType> | undefined\n        );\n      } catch (error) {\n        // Already routed to rootStore.error by the adapter; must not reject.\n        try {\n          options?.onError?.(error);\n        } catch {\n          /* caller-supplied callback errors must not crash the submit */\n        }\n      }\n      return;\n    }\n\n    // Only once this submit will actually dispatch a command. Enqueue /\n    // reject return above so they cannot reset the in-flight run's\n    // interrupt replay barrier or drop events buffered for it.\n    this.#onSubmitStart();\n\n    // Rollback: abort the previous run before starting a new one.\n    this.#runAbort?.abort();\n    const abort = new AbortController();\n    this.#runAbort = abort;\n\n    // Claim the in-flight slot before awaiting the root pump so\n    // concurrent `enqueue` submits in the same tick observe\n    // `hasActiveRun` and land in {@link queueStore}.\n    this.#rootStore.setState((s) => ({\n      ...s,\n      interrupts: [],\n      interrupt: undefined,\n      error: undefined,\n      isLoading: true,\n    }));\n\n    // Declared before the try so the catch/finally can settle the\n    // submit lifecycle (loading flag, abort slot, optimistic state)\n    // even if optimistic preparation or the pump wait throws.\n    let optimisticHandle: OptimisticHandle | undefined;\n    let dispatchInput: unknown = input;\n    let createdRunId: string | undefined;\n    let pendingCompletionReason: RunExecutionReason | undefined;\n    let completionNotified = false;\n    let settleEvent: TerminalResult[\"event\"] | undefined;\n    const notifyCompletion = (reason: RunExecutionReason): void => {\n      if (completionNotified) return;\n      if (createdRunId == null) {\n        pendingCompletionReason = reason;\n        return;\n      }\n      completionNotified = true;\n      this.#onRunCompleted(reason, createdRunId);\n    };\n    const reportError = (error: unknown): void => {\n      if (abort.signal.aborted) return;\n      this.#rootStore.setState((s) => ({ ...s, error }));\n      try {\n        options?.onError?.(error);\n      } catch {\n        /* caller-supplied callback errors must not crash the submit */\n      }\n    };\n\n    try {\n      // Apply the input optimistically *before* the first await so the\n      // user's message (and any merged state) paints without waiting for\n      // the server round-trip. Kept as the first statement in the try so\n      // the synchronous paint still precedes the first `await`, while a\n      // synchronous coercion failure (e.g. a malformed message entry)\n      // settles the submit lifecycle through the catch/finally below —\n      // exactly like a dispatch failure — instead of wedging `isLoading`\n      // / `#runAbort` and stranding later enqueue/reject submits behind a\n      // phantom in-flight run. Runs only on the dispatched path — an\n      // `\"enqueue\"`d submission returns above and echoes when it drains,\n      // keeping one optimistic batch bound to exactly one run lifecycle.\n      // `dispatchInput` carries the minted ids the server must echo for\n      // reconciliation, so the run is dispatched with it (not raw input).\n      const prepared = this.#beginOptimistic(input);\n      if (prepared != null) {\n        optimisticHandle = prepared.handle;\n        dispatchInput = prepared.dispatchInput;\n      }\n\n      // Wait for the root subscription to be live; otherwise the\n      // dispatch could resolve before we're listening for events and\n      // we'd miss the terminal that ends the run.\n      await this.#waitForRootPumpReady();\n\n      const boundConfig = bindThreadConfig(options?.config, currentThreadId);\n      // Subscribe to the next terminal *before* dispatching so a fast\n      // run's terminal can't race us.\n      const terminalPromise = this.#awaitNextTerminal(abort.signal);\n      this.#onRunStart();\n\n      let terminalSettled = false;\n      let terminal: TerminalResult | undefined;\n\n      const commandPromise = thread.submitRun({\n        input: dispatchInput ?? null,\n        config: boundConfig,\n        metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n        forkFrom: options?.forkFrom,\n        multitaskStrategy:\n          options?.multitaskStrategy === \"enqueue\"\n            ? \"enqueue\"\n            : options?.multitaskStrategy,\n      });\n      // Start the deferred root pump *after* the dispatch HTTP\n      // response lands — that's when the thread row exists server-\n      // side. Doing it synchronously here would race the response\n      // and the pump's `subscription.subscribe` would 404. Same\n      // reason we drop the self-created flag only after dispatch:\n      // future hydrates need the thread to exist before they fetch\n      // state.\n      //\n      // Fire-and-forget: we don't want to gate Promise.race on this,\n      // and `commandPromise.catch` is already handled below. A\n      // dispatch failure means there's no thread to pump anyway.\n      void commandPromise.then(\n        () => {\n          this.#startDeferredRootPump();\n          this.#forgetSelfCreatedThreadId(activeThreadId);\n        },\n        () => {\n          // Dispatch failed. Without abandoning, `#rootPumpDeferred`\n          // stays armed and `selfCreatedThreadIds` still holds this\n          // id — a retry submit would see `pendingServerCreate=true`\n          // again but `#ensureThread` would early-return because\n          // `#thread != null`, and the root pump would never start.\n          // Tear down so the next submit re-runs `#ensureThread`\n          // from scratch. Keep the self-created mark so the retry\n          // still defers the pump (the server row was never created).\n          if (pendingServerCreate) {\n            this.#abandonDeferredRootPump();\n          }\n        }\n      );\n      const notifyCreated = (result: { run_id?: unknown }) => {\n        if (typeof result.run_id !== \"string\") return;\n        createdRunId = result.run_id;\n        this.#onRunCreated(createdRunId);\n        if (pendingCompletionReason != null) {\n          notifyCompletion(pendingCompletionReason);\n        }\n      };\n      const first = await Promise.race([\n        terminalPromise.then((value) => ({\n          type: \"terminal\" as const,\n          value,\n        })),\n        commandPromise.then(\n          (result) => ({ type: \"command\" as const, result }),\n          (error) => ({ type: \"error\" as const, error })\n        ),\n      ]);\n      if (first.type === \"error\") throw first.error;\n      if (first.type === \"command\") {\n        notifyCreated(first.result);\n      } else {\n        // Terminal landed first (very fast runs). Wait for the\n        // dispatch response in the background so onCreated fires\n        // and dispatch errors still surface.\n        terminal = first.value;\n        terminalSettled = true;\n        void commandPromise.then(notifyCreated).catch((error) => {\n          if (!terminalSettled) reportError(error);\n        });\n      }\n\n      terminal ??= await terminalPromise;\n      terminalSettled = true;\n      settleEvent = terminal.event;\n      if (terminal.event === \"failed\" && !abort.signal.aborted) {\n        const runError = new Error(\n          terminal.error ?? \"Run failed with no error message\"\n        );\n        this.#rootStore.setState((s) => ({ ...s, error: runError }));\n        try {\n          options?.onError?.(runError);\n        } catch {\n          /* caller-supplied callback errors must not crash the submit */\n        }\n      }\n      notifyCompletion(terminalReason(terminal.event));\n    } catch (error) {\n      if (!abort.signal.aborted) settleEvent = \"failed\";\n      reportError(error);\n    } finally {\n      // Always settle loading and clear our slot of the abort\n      // controller. Schedule queue drain on the next macrotask so any\n      // late state updates from this run finish flushing first.\n      this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n      if (this.#runAbort === abort) this.#runAbort = undefined;\n      // Reconcile optimistic state: flip pending messages to sent/failed\n      // and roll back un-echoed non-message keys. `aborted` covers a\n      // rollback-resubmit or `stop()` cancelling this run.\n      if (optimisticHandle != null) {\n        this.#settleOptimistic(\n          optimisticHandle,\n          abort.signal.aborted ? \"aborted\" : (settleEvent ?? \"failed\")\n        );\n      }\n      this.#onRunEnd();\n      this.#scheduleQueueDrain();\n    }\n  }\n\n  /**\n   * Schedules a queue-drain attempt on the next macrotask, once the\n   * just-finished run's state has flushed. A no-op unless the queue\n   * adapter is idle-driven (see {@link QueueAdapter.onIdle}) and no\n   * other run is active.\n   */\n  #scheduleQueueDrain(): void {\n    setTimeout(() => {\n      if (this.#getDisposed()) return;\n      if (this.#runAbort != null && !this.#runAbort.signal.aborted) return;\n      if (this.#rootStore.getSnapshot().isLoading) return;\n\n      this.#queueAdapter.onIdle?.((values, drainOptions) =>\n        this.submit(\n          values,\n          drainOptions as StreamSubmitOptions<StateType, ConfigurableType>\n        ).catch(() => {\n          /* submit() already routes errors through the per-submit onError\n           * hook and the root store; swallow here so a failing drain does\n           * not surface as an unhandled rejection. */\n        })\n      );\n    }, 0);\n  }\n\n  /**\n   * Surface a *resumed* run's failure the same way {@link submit} surfaces\n   * a fresh run's failure — by writing it to the reactive\n   * {@link RootSnapshot.error} slot.\n   *\n   * `respond()` / `respondAll()` dispatch their `input.respond` command on\n   * the controller directly (they target a specific interrupt, so they\n   * cannot go through {@link submit}, which only does `run.start`). The\n   * resumed run therefore never passed through the submit lifecycle that\n   * populates `rootStore.error` — only the persistent lifecycle listener\n   * observed it, and that listener drives `isLoading` alone. Without this,\n   * a resumed run that fails (e.g. a missing model key surfaced after the\n   * user approves an interrupt) would flip `isLoading` back to `false`\n   * with `error` left untouched, so `stream.error`-driven UIs (error\n   * banners, API-key retry prompts) would silently miss it.\n   *\n   * The `dispatch` thunk is awaited, so a dispatch failure rejects the\n   * caller's `respond()` *and* lands in `rootStore.error`. The resumed\n   * run's terminal is watched in the **background** so the returned promise\n   * still settles on dispatch — preserving the resume command's\n   * resolve-on-dispatch contract (and avoiding a hang when no terminal is\n   * ever emitted, e.g. in unit tests).\n   *\n   * Reuses the shared {@link #runAbort} slot, so `stop()`, `dispose()`, and\n   * a rollback `submit()` all cancel the terminal watch (no spurious error\n   * on user-initiated cancel) and treat the resumed run as the active run.\n   *\n   * The terminal watch uses {@link #awaitResumedRunTerminal}, which skips\n   * stale `interrupted` terminals from the run being resumed (they can reach\n   * the pump after `input.requested` but before `respondInput` calls\n   * `#prepareForNextRun`) and only accepts a later `interrupted` once a\n   * root `running` lifecycle for the resumed run has been observed.\n   *\n   * @param dispatch - Sends the `input.respond` command (and marks the\n   *   targeted interrupt resolved). Invoked after the terminal watch is\n   *   armed.\n   * @param optimisticHandle - Optional handle from an optimistic `update`\n   *   applied before dispatch (HITL \"push card into state + resume\"). Settled\n   *   on the resumed run's terminal — pending messages flip to `sent` and\n   *   un-echoed non-message keys roll back on failure — exactly like the\n   *   `submit()` optimistic lifecycle. A dispatch failure settles it `failed`.\n   */\n  async dispatchResume(\n    dispatch: () => Promise<void>,\n    optimisticHandle?: OptimisticHandle\n  ): Promise<void> {\n    if (this.#getDisposed()) return;\n\n    // Same allowlist clear as submit(): a resumed run can pause on a\n    // *new* interrupt id. Without this, the hydrate-window allowlist\n    // still contains only the interrupt being answered and\n    // `#recordRootInterrupt` drops the follow-on `input.requested`.\n    this.#onSubmitStart();\n\n    // Rollback any run still tracked as active (mirrors submit()), then\n    // claim the in-flight slot so stop()/dispose()/a concurrent submit\n    // cancels the terminal watch armed below.\n    this.#runAbort?.abort();\n    const abort = new AbortController();\n    this.#runAbort = abort;\n\n    // Optimistically clear a stale error from a previous run, matching\n    // submit()'s reset, so the resume starts from a clean error slot.\n    this.#rootStore.setState((s) =>\n      s.error === undefined ? s : { ...s, error: undefined }\n    );\n\n    const reportError = (error: unknown): void => {\n      if (abort.signal.aborted) return;\n      this.#rootStore.setState((s) => ({ ...s, error }));\n    };\n\n    // Settle the optimistic `update` exactly once, whether the resumed run\n    // terminates (success/failure/interrupt) or the dispatch itself fails.\n    let optimisticSettled = false;\n    const settleOptimisticOnce = (event: TerminalResult[\"event\"]): void => {\n      if (optimisticSettled || optimisticHandle == null) return;\n      optimisticSettled = true;\n      this.#settleOptimistic(optimisticHandle, event);\n    };\n\n    // Subscribe to the resumed run's terminal *before* dispatching so a fast\n    // `failed` can't race us. Unlike `#awaitNextTerminal`, the resume watcher\n    // ignores stale `interrupted` events until root `running` is seen.\n    // Watched in the background — we never gate the returned promise on the\n    // resumed run's terminal.\n    const terminalPromise = this.#awaitResumedRunTerminal(abort.signal);\n    void terminalPromise.then((terminal) => {\n      if (this.#runAbort === abort) this.#runAbort = undefined;\n      if (terminal.event === \"failed\" && !abort.signal.aborted) {\n        reportError(\n          new Error(terminal.error ?? \"Run failed with no error message\")\n        );\n      }\n      settleOptimisticOnce(abort.signal.aborted ? \"aborted\" : terminal.event);\n      // Drain any submission enqueued while the resumed run was active.\n      this.#scheduleQueueDrain();\n    });\n\n    try {\n      await dispatch();\n    } catch (error) {\n      // The `input.respond` send itself failed, before any run started.\n      reportError(error);\n      settleOptimisticOnce(\"failed\");\n      if (this.#runAbort === abort) this.#runAbort = undefined;\n      throw error;\n    }\n  }\n\n  /**\n   * Abort the current run (if any) and force `isLoading=false`.\n   *\n   * Client-side only — server-side cancel is handled by\n   * {@link StreamController.stop} before this is invoked.\n   */\n  async stop(): Promise<void> {\n    this.abortActiveRun();\n    this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n  }\n\n  /**\n   * Abort the current run without forcing the loading flag down.\n   *\n   * Used by {@link StreamController.dispose}: disposal already tears\n   * down the root store, so flipping `isLoading` here is unnecessary\n   * and would race the dispose path.\n   */\n  abortActiveRun(): void {\n    this.#runAbort?.abort();\n    this.#runAbort = undefined;\n  }\n\n  /**\n   * Populate the queue from whatever the configured adapter considers\n   * the source of truth. No-op for `LocalQueueAdapter`; for the\n   * server-backed adapter this is what makes pending runs created\n   * before this page load (or by another tab/session) show up.\n   * Fire-and-forget from the caller's side: the adapter manages its\n   * own staleness guard against a rapid, repeated call for a newer\n   * thread.\n   */\n  async hydrateQueue(threadId: string): Promise<void> {\n    await this.#queueAdapter.hydrate(threadId);\n  }\n\n  /**\n   * Public entry point for {@link #scheduleQueueDrain}. Called by the\n   * controller when `isLoading` settles to `false` for a run this\n   * coordinator never dispatched itself.\n   */\n  scheduleQueueDrainOnObservedIdle(): void {\n    this.#scheduleQueueDrain();\n  }\n\n  /**\n   * Cancel a queued submission by id.\n   *\n   * @param id - Client-side queue entry id to remove.\n   * @returns `true` when the entry was found and dropped, `false` otherwise.\n   */\n  async cancelQueued(id: string): Promise<boolean> {\n    return this.#queueAdapter.cancel(id);\n  }\n\n  /**\n   * Drop every queued submission. Cancels the underlying server-side\n   * runs too when server-backed; see `AgentServerQueueAdapter.clear`.\n   */\n  async clearQueue(): Promise<void> {\n    await this.#queueAdapter.clear();\n  }\n\n  /**\n   * Release whatever the queue adapter is watching. Called by\n   * {@link StreamController} on thread rebind and on dispose, alongside\n   * its own reset of the (still directly-owned, reactive) queue store.\n   */\n  detachQueue(): void {\n    this.#queueAdapter.detach();\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,SAAS,eAAe,OAAoD;AAC1E,KAAI,UAAU,YAAa,QAAO;AAClC,KAAI,UAAU,SAAU,QAAO;AAC/B,KAAI,UAAU,cAAe,QAAO;AACpC,QAAO;;;AAIT,SAAS,4BACP,OACwC;AAOxC,KAAI,EALF,OAAO,UAAU,YACjB,SAAS,QACT,OAAQ,MAAgC,WAAW,cACnD,OAAQ,MAAgC,SAAS,cACjD,OAAQ,MAAgC,WAAW,YAEnD,OAAM,IAAI,MACR,yEACD;;;;;;;;;;;;;;;;;;;;AAsBL,IAAa,oBAAb,MAIE;;CAEA;;CAEA;;;CAGA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAKA;;CAEA;;CAEA;;CAEA;;;;;CAKA;;CAIA;;CAEA;;CAEA;;CAEA;;CAKA;;;;;;;CAOA;;CAIA;;;;;;;;CAYA;CAEA,YAAY,QA6BT;AACD,QAAA,UAAgB,OAAO;AACvB,QAAA,YAAkB,OAAO;EACzB,MAAM,gBAAgB,UAAmB;AACvC,SAAA,UAAgB,UAAU,WAAW;IAAE,GAAG;IAAO;IAAO,EAAE;;EAK5D,MAAM,qBACJ,OAAO,OAAO,QAAQ,cAAc,YACpC,OAAO,QAAQ,UAAU;AAC3B,MAAI,mBACF,6BAA4B,OAAO,QAAQ,OAAO,KAAK;AAEzD,QAAA,eAAqB,qBACjB,IAAIuB,mCAAAA,wBACF,OAAO,QAAQ,OAAO,MACtB,OAAO,QAAQ,aACf,OAAO,YACP,eACC,aAAa,OAAO,aAAa,UAAU,KAAK,CAClD,GACD,IAAIC,4BAAAA,kBAAkB,OAAO,YAAY,aAAa;AAC1D,QAAA,cAAoB,OAAO;AAC3B,QAAA,qBAA2B,OAAO;AAClC,QAAA,qBAA2B,OAAO;AAClC,QAAA,8BAAoC,OAAO;AAC3C,QAAA,wBAA8B,OAAO;AACrC,QAAA,4BAAkC,OAAO;AACzC,QAAA,UAAgB,OAAO;AACvB,QAAA,eAAqB,OAAO;AAC5B,QAAA,wBAA8B,OAAO;AACrC,QAAA,0BAAgC,OAAO;AACvC,QAAA,uBAA6B,OAAO;AACpC,QAAA,oBAA0B,OAAO;AACjC,QAAA,0BAAgC,OAAO;AACvC,QAAA,gBAAsB,OAAO,wBAAwB,KAAA;AACrD,QAAA,aAAmB,OAAO,qBAAqB,KAAA;AAC/C,QAAA,eAAqB,OAAO,uBAAuB,KAAA;AACnD,QAAA,iBAAuB,OAAO,yBAAyB,KAAA;AACvD,QAAA,WAAiB,OAAO,mBAAmB,KAAA;AAC3C,QAAA,kBAAwB,OAAO,0BAA0B,KAAA;AACzD,QAAA,mBAAyB,OAAO,2BAA2B,KAAA;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B7D,MAAM,OACJ,OACA,SACe;AACf,MAAI,MAAA,aAAmB,CAAE;EAIzB,MAAM,mBAAmB,SAAS;AAClC,MACE,qBAAqB,KAAA,KACrB,qBAAqB,MAAA,oBAA0B,CAE/C,OAAM,MAAA,QAAc,iBAAiB;EAMvC,MAAM,iBAAiB,MAAA,oBAA0B,IAAI;AACrD,MAAI,gBAAgB;GAClB,MAAM,YAAA,GAAA,2BAAA,KAAmB;AACzB,SAAA,mBAAyB,SAAS;AAClC,SAAA,4BAAkC,SAAS;AAC3C,SAAA,QAAc,aAAa,SAAS;AACpC,SAAA,UAAgB,UAAU,OAAO;IAC/B,GAAG;IACH;IACD,EAAE;;EAGL,MAAM,kBAAkB,MAAA,oBAA0B;AAClD,MAAI,mBAAmB,KAAM;EAU7B,MAAM,sBACJ,kBAAkB,MAAA,sBAA4B,gBAAgB;EAChE,MAAM,SAAS,MAAA,aAAmB,iBAAiB,oBAAoB;EACvE,MAAM,iBAAiB;EAEvB,MAAM,WAAW,SAAS,qBAAqB;EAa/C,MAAM,eACJ,CAAC,mBACC,MAAA,YAAkB,QAAQ,CAAC,MAAA,SAAe,OAAO,WACjD,MAAA,UAAgB,aAAa,CAAC;AAClC,MAAI,gBAAgB,aAAa,SAC/B,OAAM,IAAI,MACR,mFACD;AAEH,MAAI,gBAAgB,aAAa,WAAW;AAC1C,OAAI;AACF,UAAM,MAAA,aAAmB,QACvB,iBACC,SAAS,KAAA,GAEV,QACD;YACM,OAAO;AAEd,QAAI;AACF,cAAS,UAAU,MAAM;YACnB;;AAIV;;AAMF,QAAA,eAAqB;AAGrB,QAAA,UAAgB,OAAO;EACvB,MAAM,QAAQ,IAAI,iBAAiB;AACnC,QAAA,WAAiB;AAKjB,QAAA,UAAgB,UAAU,OAAO;GAC/B,GAAG;GACH,YAAY,EAAE;GACd,WAAW,KAAA;GACX,OAAO,KAAA;GACP,WAAW;GACZ,EAAE;EAKH,IAAI;EACJ,IAAI,gBAAyB;EAC7B,IAAI;EACJ,IAAI;EACJ,IAAI,qBAAqB;EACzB,IAAI;EACJ,MAAM,oBAAoB,WAAqC;AAC7D,OAAI,mBAAoB;AACxB,OAAI,gBAAgB,MAAM;AACxB,8BAA0B;AAC1B;;AAEF,wBAAqB;AACrB,SAAA,eAAqB,QAAQ,aAAa;;EAE5C,MAAM,eAAe,UAAyB;AAC5C,OAAI,MAAM,OAAO,QAAS;AAC1B,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG;IAAO,EAAE;AAClD,OAAI;AACF,aAAS,UAAU,MAAM;WACnB;;AAKV,MAAI;GAcF,MAAM,WAAW,MAAA,gBAAsB,MAAM;AAC7C,OAAI,YAAY,MAAM;AACpB,uBAAmB,SAAS;AAC5B,oBAAgB,SAAS;;AAM3B,SAAM,MAAA,sBAA4B;GAElC,MAAM,cAAcE,wBAAAA,iBAAiB,SAAS,QAAQ,gBAAgB;GAGtE,MAAM,kBAAkB,MAAA,kBAAwB,MAAM,OAAO;AAC7D,SAAA,YAAkB;GAElB,IAAI,kBAAkB;GACtB,IAAI;GAEJ,MAAM,iBAAiB,OAAO,UAAU;IACtC,OAAO,iBAAiB;IACxB,QAAQ;IACR,UAAW,SAAS,YAAY,KAAA;IAChC,UAAU,SAAS;IACnB,mBACE,SAAS,sBAAsB,YAC3B,YACA,SAAS;IAChB,CAAC;AAYG,kBAAe,WACZ;AACJ,UAAA,uBAA6B;AAC7B,UAAA,0BAAgC,eAAe;YAE3C;AASJ,QAAI,oBACF,OAAA,yBAA+B;KAGpC;GACD,MAAM,iBAAiB,WAAiC;AACtD,QAAI,OAAO,OAAO,WAAW,SAAU;AACvC,mBAAe,OAAO;AACtB,UAAA,aAAmB,aAAa;AAChC,QAAI,2BAA2B,KAC7B,kBAAiB,wBAAwB;;GAG7C,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAC/B,gBAAgB,MAAM,WAAW;IAC/B,MAAM;IACN;IACD,EAAE,EACH,eAAe,MACZ,YAAY;IAAE,MAAM;IAAoB;IAAQ,IAChD,WAAW;IAAE,MAAM;IAAkB;IAAO,EAC9C,CACF,CAAC;AACF,OAAI,MAAM,SAAS,QAAS,OAAM,MAAM;AACxC,OAAI,MAAM,SAAS,UACjB,eAAc,MAAM,OAAO;QACtB;AAIL,eAAW,MAAM;AACjB,sBAAkB;AACb,mBAAe,KAAK,cAAc,CAAC,OAAO,UAAU;AACvD,SAAI,CAAC,gBAAiB,aAAY,MAAM;MACxC;;AAGJ,gBAAa,MAAM;AACnB,qBAAkB;AAClB,iBAAc,SAAS;AACvB,OAAI,SAAS,UAAU,YAAY,CAAC,MAAM,OAAO,SAAS;IACxD,MAAM,WAAW,IAAI,MACnB,SAAS,SAAS,mCACnB;AACD,UAAA,UAAgB,UAAU,OAAO;KAAE,GAAG;KAAG,OAAO;KAAU,EAAE;AAC5D,QAAI;AACF,cAAS,UAAU,SAAS;YACtB;;AAIV,oBAAiB,eAAe,SAAS,MAAM,CAAC;WACzC,OAAO;AACd,OAAI,CAAC,MAAM,OAAO,QAAS,eAAc;AACzC,eAAY,MAAM;YACV;AAIR,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG,WAAW;IAAO,EAAE;AAC7D,OAAI,MAAA,aAAmB,MAAO,OAAA,WAAiB,KAAA;AAI/C,OAAI,oBAAoB,KACtB,OAAA,iBACE,kBACA,MAAM,OAAO,UAAU,YAAa,eAAe,SACpD;AAEH,SAAA,UAAgB;AAChB,SAAA,oBAA0B;;;;;;;;;CAU9B,sBAA4B;AAC1B,mBAAiB;AACf,OAAI,MAAA,aAAmB,CAAE;AACzB,OAAI,MAAA,YAAkB,QAAQ,CAAC,MAAA,SAAe,OAAO,QAAS;AAC9D,OAAI,MAAA,UAAgB,aAAa,CAAC,UAAW;AAE7C,SAAA,aAAmB,UAAU,QAAQ,iBACnC,KAAK,OACH,QACA,aACD,CAAC,YAAY,GAIZ,CACH;KACA,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CP,MAAM,eACJ,UACA,kBACe;AACf,MAAI,MAAA,aAAmB,CAAE;AAMzB,QAAA,eAAqB;AAKrB,QAAA,UAAgB,OAAO;EACvB,MAAM,QAAQ,IAAI,iBAAiB;AACnC,QAAA,WAAiB;AAIjB,QAAA,UAAgB,UAAU,MACxB,EAAE,UAAU,KAAA,IAAY,IAAI;GAAE,GAAG;GAAG,OAAO,KAAA;GAAW,CACvD;EAED,MAAM,eAAe,UAAyB;AAC5C,OAAI,MAAM,OAAO,QAAS;AAC1B,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG;IAAO,EAAE;;EAKpD,IAAI,oBAAoB;EACxB,MAAM,wBAAwB,UAAyC;AACrE,OAAI,qBAAqB,oBAAoB,KAAM;AACnD,uBAAoB;AACpB,SAAA,iBAAuB,kBAAkB,MAAM;;AAQzB,QAAA,wBAA8B,MAAM,OAAO,CAC9C,MAAM,aAAa;AACtC,OAAI,MAAA,aAAmB,MAAO,OAAA,WAAiB,KAAA;AAC/C,OAAI,SAAS,UAAU,YAAY,CAAC,MAAM,OAAO,QAC/C,aACE,IAAI,MAAM,SAAS,SAAS,mCAAmC,CAChE;AAEH,wBAAqB,MAAM,OAAO,UAAU,YAAY,SAAS,MAAM;AAEvE,SAAA,oBAA0B;IAC1B;AAEF,MAAI;AACF,SAAM,UAAU;WACT,OAAO;AAEd,eAAY,MAAM;AAClB,wBAAqB,SAAS;AAC9B,OAAI,MAAA,aAAmB,MAAO,OAAA,WAAiB,KAAA;AAC/C,SAAM;;;;;;;;;CAUV,MAAM,OAAsB;AAC1B,OAAK,gBAAgB;AACrB,QAAA,UAAgB,UAAU,OAAO;GAAE,GAAG;GAAG,WAAW;GAAO,EAAE;;;;;;;;;CAU/D,iBAAuB;AACrB,QAAA,UAAgB,OAAO;AACvB,QAAA,WAAiB,KAAA;;;;;;;;;;;CAYnB,MAAM,aAAa,UAAiC;AAClD,QAAM,MAAA,aAAmB,QAAQ,SAAS;;;;;;;CAQ5C,mCAAyC;AACvC,QAAA,oBAA0B;;;;;;;;CAS5B,MAAM,aAAa,IAA8B;AAC/C,SAAO,MAAA,aAAmB,OAAO,GAAG;;;;;;CAOtC,MAAM,aAA4B;AAChC,QAAM,MAAA,aAAmB,OAAO;;;;;;;CAQlC,cAAoB;AAClB,QAAA,aAAmB,QAAQ"}