{"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#buildResumeRunInput","#markInterruptResolved","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","#runAbort","#enqueueSubmission","#drainQueue"],"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()` or\n *      `thread.submitRun()` for resumes) 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 recorded\n * into {@link queueStore} and the call returns immediately. After the\n * active run terminates, `#drainQueue` schedules the head of the\n * queue as a fresh submit on the next macrotask. Each drained\n * submission has its own `multitaskStrategy` cleared so it doesn't\n * recursively re-enqueue.\n *\n * @see StreamController - The owner; injects every collaborator dep.\n */\nimport { v7 as uuidv7 } from \"uuid\";\nimport type { ThreadStream } from \"../client/stream/index.js\";\nimport { StreamStore } from \"./store.js\";\nimport type {\n  RootSnapshot,\n  RunExecutionReason,\n  StreamControllerOptions,\n  StreamSubmitOptions,\n} from \"./types.js\";\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/**\n * Queued submission entry mirrored from the server-side run queue.\n *\n * Surfaces the deferred submission to UI consumers via\n * {@link StreamController.queueStore}.\n */\nexport interface SubmissionQueueEntry<\n  StateType extends object = Record<string, unknown>,\n> {\n  /** Stable id minted on enqueue (uuidv7 — sortable by creation time). */\n  readonly id: string;\n  /** Original submit input, narrowed to the partial state shape. */\n  readonly values: Partial<StateType> | null | undefined;\n  /** Original submit options, minus the strategy slot which is reset on drain. */\n  readonly options?: StreamSubmitOptions<StateType>;\n  /** Wall-clock timestamp at enqueue. */\n  readonly createdAt: Date;\n}\n\n/**\n * Read-only snapshot of the queue. The queue store hands this out\n * directly; consumers must not mutate the array.\n */\nexport type SubmissionQueueSnapshot<\n  StateType extends object = Record<string, unknown>,\n> = ReadonlyArray<SubmissionQueueEntry<StateType>>;\n\n/**\n * Frozen empty queue value used as the initial / cleared snapshot.\n *\n * Reusing one frozen reference keeps store identity stable across\n * empty resets, so React's `useSyncExternalStore` doesn't think the\n * queue changed when it actually didn't.\n */\nexport const EMPTY_QUEUE: SubmissionQueueSnapshot<never> = Object.freeze([]);\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  readonly #queueStore: StreamStore<SubmissionQueueSnapshot<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  /** 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  /** Builds interrupt-id keyed `run.start` input from `command.resume`. */\n  readonly #buildResumeRunInput: (\n    resume: unknown\n  ) => Record<string, unknown> | null;\n  /** Marks an interrupt id as resolved so it isn't re-targeted. */\n  readonly #markInterruptResolved: (interruptId: string) => void;\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  /**\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    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    buildResumeRunInput: (resume: unknown) => Record<string, unknown> | null;\n    markInterruptResolved: (interruptId: string) => void;\n    onSubmitStart?: () => void;\n    onRunStart?: () => void;\n    onRunCreated?: (runId: string) => void;\n    onRunCompleted?: (reason: RunExecutionReason, runId?: string) => void;\n    onRunEnd?: () => void;\n  }) {\n    this.#options = params.options;\n    this.#rootStore = params.rootStore;\n    this.#queueStore = params.queueStore;\n    this.#getDisposed = params.getDisposed;\n    this.#getCurrentThreadId = params.getCurrentThreadId;\n    this.#setCurrentThreadId = params.setCurrentThreadId;\n    this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;\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.#buildResumeRunInput = params.buildResumeRunInput;\n    this.#markInterruptResolved = params.markInterruptResolved;\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  }\n\n  /**\n   * Submit input or a resume command 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 #enqueueSubmission};\n   *     the call returns without dispatching.\n   *   - `\"interrupt\"`           — falls through to the default path\n   *     (server-side cancellation lands with roadmap A0.3).\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   * @param input   - Input payload, or `null`/`undefined` for no input\n   *   (typical for resume commands).\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    this.#onSubmitStart();\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 client-self-created threads we defer the persistent root SSE\n    // pump until after `submitRun` / `respondInput` commits the thread\n    // server-side. Opening the pump's `subscription.subscribe` against\n    // a not-yet-existent thread row produces a `404: Thread not found`\n    // protocol error that strands lifecycle / messages events for the\n    // first run. The deferred path starts the pump after dispatch\n    // returns (see `#startDeferredRootPump` calls below).\n    const thread = this.#ensureThread(currentThreadId, wasSelfCreated);\n    const activeThreadId = currentThreadId;\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 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    const hasActiveRun =\n      !wasSelfCreated &&\n      this.#runAbort != null &&\n      !this.#runAbort.signal.aborted;\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      this.#enqueueSubmission(input, options);\n      return;\n    }\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    const resumeCommand = options?.command?.resume;\n    const isResume = resumeCommand !== undefined;\n\n    // Optimistically clear interrupts/error and flip loading. The\n    // root pump's lifecycle listener will re-flip these as the run\n    // terminates.\n    this.#rootStore.setState((s) => ({\n      ...s,\n      interrupts: [],\n      interrupt: undefined,\n      error: undefined,\n      isLoading: true,\n    }));\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 createdRunId: string | undefined;\n    let pendingCompletionReason: RunExecutionReason | undefined;\n    let completionNotified = false;\n    const notifyCompletion = (reason: RunExecutionReason): void => {\n      if (completionNotified) return;\n      if (!isResume && 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      let terminal: TerminalResult | undefined;\n\n      if (isResume) {\n        const resumeInput = this.#buildResumeRunInput(resumeCommand);\n        if (resumeInput == null) {\n          throw new Error(\n            \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n          );\n        }\n        const commandPromise = thread.submitRun({\n          input: resumeInput,\n          config: boundConfig,\n          metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n        });\n        // Defer the pump start until the dispatch HTTP response\n        // lands — see the analogous block in the non-resume path\n        // below for the rationale (thread row not committed\n        // synchronously). For a resume the thread exists already\n        // (it must, since `buildResumeRunInput()` was non-null),\n        // so `#startDeferredRootPump` is typically a no-op here, but\n        // we keep the same shape to avoid a future regression.\n        //\n        // Asymmetry with the non-resume path: we don't call\n        // `#forgetSelfCreatedThreadId` because a resume implies the\n        // thread already committed, so the self-created marker was\n        // already cleared on the original submit. Same reason we\n        // don't call `#abandonDeferredRootPump` on failure here.\n        void commandPromise.then(\n          () => this.#startDeferredRootPump(),\n          () => {}\n        );\n        // Mark resolved synchronously: even if the response races and\n        // the command settles after the terminal, we don't want to\n        // re-target these interrupts on the next submit.\n        for (const interruptId of Object.keys(resumeInput)) {\n          this.#markInterruptResolved(interruptId);\n        }\n        const first = await Promise.race([\n          terminalPromise.then((value) => ({\n            type: \"terminal\" as const,\n            value,\n          })),\n          commandPromise.then(\n            () => ({ type: \"command\" as const }),\n            (error) => ({ type: \"error\" as const, error })\n          ),\n        ]);\n        if (first.type === \"error\") throw first.error;\n        if (first.type === \"terminal\") {\n          terminal = first.value;\n          terminalSettled = true;\n          // Stale command response — surface as error only if it\n          // arrives with a real failure (not just our own abort).\n          void commandPromise.catch((error) => {\n            if (!terminalSettled) reportError(error);\n          });\n        }\n      } else {\n        const commandPromise = thread.submitRun({\n          input: input ?? 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 `wasSelfCreated=false`\n            // (currentThreadId is no longer null), `#ensureThread`\n            // would early-return because `#thread != null`, and the\n            // root pump would never start. Tear down so the next\n            // submit re-runs `#ensureThread` from scratch.\n            if (wasSelfCreated) {\n              this.#abandonDeferredRootPump();\n              this.#forgetSelfCreatedThreadId(activeThreadId);\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\n      terminal ??= await terminalPromise;\n      terminalSettled = true;\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      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      this.#onRunEnd();\n      setTimeout(() => this.#drainQueue(), 0);\n    }\n  }\n\n  /**\n   * Abort the current run (if any) and force `isLoading=false`.\n   *\n   * Does NOT issue a server-side cancel — that lands with roadmap\n   * A0.3. Today this is a client-side stop only: subsequent events\n   * for the aborted run are ignored by the controller's pump because\n   * the abort signal is the same one `#awaitNextTerminal` is wired\n   * to.\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   * 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    const current = this.#queueStore.getSnapshot();\n    const next = current.filter((entry) => entry.id !== id);\n    if (next.length === current.length) return false;\n    this.#queueStore.setState(() => next);\n    return true;\n  }\n\n  /**\n   * Drop every queued submission. Server-side cancel arrives with A0.3.\n   */\n  async clearQueue(): Promise<void> {\n    this.#queueStore.setState(\n      () => EMPTY_QUEUE as SubmissionQueueSnapshot<StateType>\n    );\n  }\n\n  /**\n   * Append a submission to the queue without dispatching.\n   *\n   * The drained submission is later run via {@link #drainQueue} after\n   * the active run terminates.\n   */\n  #enqueueSubmission(\n    input: unknown,\n    options?: StreamSubmitOptions<StateType, ConfigurableType>\n  ): void {\n    const entry: SubmissionQueueEntry<StateType> = {\n      id: uuidv7(),\n      values: (input ?? undefined) as Partial<StateType> | null | undefined,\n      options: options as StreamSubmitOptions<StateType> | undefined,\n      createdAt: new Date(),\n    };\n    this.#queueStore.setState((current) => [...current, entry]);\n  }\n\n  /**\n   * Drain the head of the queue if no run is active.\n   *\n   * Called from the `finally` block of `submit()` on the next\n   * macrotask (so the just-finished run's state flushes first).\n   * Strips the strategy off the dequeued options to prevent infinite\n   * re-enqueueing.\n   */\n  #drainQueue(): void {\n    if (this.#getDisposed()) return;\n    if (this.#runAbort != null && !this.#runAbort.signal.aborted) return;\n    const current = this.#queueStore.getSnapshot();\n    if (current.length === 0) return;\n    const [next, ...rest] = current;\n    this.#queueStore.setState(() => rest);\n    const nextOptions: StreamSubmitOptions<StateType, ConfigurableType> = {\n      ...((next.options ?? {}) as StreamSubmitOptions<\n        StateType,\n        ConfigurableType\n      >),\n      multitaskStrategy: undefined,\n    };\n    void this.submit(next.values, nextOptions).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}\n\n/**\n * Merge `thread_id` into a user-supplied `config.configurable` blob.\n *\n * The platform expects `config.configurable.thread_id` on every run\n * dispatch; we set it last so user-supplied values can't accidentally\n * override the active thread id (which would route the run to a\n * different thread).\n */\nfunction bindThreadConfig(\n  config: unknown,\n  threadId: string\n): Record<string, unknown> {\n  const base =\n    config != null && typeof config === \"object\"\n      ? (config as Record<string, unknown>)\n      : {};\n  const configurable =\n    base.configurable != null && typeof base.configurable === \"object\"\n      ? (base.configurable as Record<string, unknown>)\n      : {};\n  return {\n    ...base,\n    configurable: {\n      ...configurable,\n      thread_id: threadId,\n    },\n  };\n}\n"],"mappings":"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"}