@langchain/langgraph-sdk 1.9.1 → 1.9.3-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/client/crons/index.cjs +8 -2
  2. package/dist/client/crons/index.cjs.map +1 -1
  3. package/dist/client/crons/index.d.cts +7 -1
  4. package/dist/client/crons/index.d.cts.map +1 -1
  5. package/dist/client/crons/index.d.ts +7 -1
  6. package/dist/client/crons/index.d.ts.map +1 -1
  7. package/dist/client/crons/index.js +8 -2
  8. package/dist/client/crons/index.js.map +1 -1
  9. package/dist/client/stream/index.cjs +103 -14
  10. package/dist/client/stream/index.cjs.map +1 -1
  11. package/dist/client/stream/index.d.cts +21 -1
  12. package/dist/client/stream/index.d.cts.map +1 -1
  13. package/dist/client/stream/index.d.ts +21 -1
  14. package/dist/client/stream/index.d.ts.map +1 -1
  15. package/dist/client/stream/index.js +103 -14
  16. package/dist/client/stream/index.js.map +1 -1
  17. package/dist/client/stream/messages.cjs +50 -10
  18. package/dist/client/stream/messages.cjs.map +1 -1
  19. package/dist/client/stream/messages.d.cts +4 -0
  20. package/dist/client/stream/messages.d.cts.map +1 -1
  21. package/dist/client/stream/messages.d.ts +4 -0
  22. package/dist/client/stream/messages.d.ts.map +1 -1
  23. package/dist/client/stream/messages.js +50 -10
  24. package/dist/client/stream/messages.js.map +1 -1
  25. package/dist/client/stream/transport.d.cts +12 -0
  26. package/dist/client/stream/transport.d.cts.map +1 -1
  27. package/dist/client/stream/transport.d.ts +12 -0
  28. package/dist/client/stream/transport.d.ts.map +1 -1
  29. package/dist/stream/assembled-to-message.cjs +10 -6
  30. package/dist/stream/assembled-to-message.cjs.map +1 -1
  31. package/dist/stream/assembled-to-message.d.cts.map +1 -1
  32. package/dist/stream/assembled-to-message.d.ts.map +1 -1
  33. package/dist/stream/assembled-to-message.js +10 -6
  34. package/dist/stream/assembled-to-message.js.map +1 -1
  35. package/dist/stream/controller.cjs +163 -13
  36. package/dist/stream/controller.cjs.map +1 -1
  37. package/dist/stream/controller.d.cts.map +1 -1
  38. package/dist/stream/controller.d.ts.map +1 -1
  39. package/dist/stream/controller.js +163 -13
  40. package/dist/stream/controller.js.map +1 -1
  41. package/dist/stream/discovery/subagents.cjs +31 -0
  42. package/dist/stream/discovery/subagents.cjs.map +1 -1
  43. package/dist/stream/discovery/subagents.d.cts.map +1 -1
  44. package/dist/stream/discovery/subagents.d.ts.map +1 -1
  45. package/dist/stream/discovery/subagents.js +31 -0
  46. package/dist/stream/discovery/subagents.js.map +1 -1
  47. package/dist/stream/message-reconciliation.cjs +11 -1
  48. package/dist/stream/message-reconciliation.cjs.map +1 -1
  49. package/dist/stream/message-reconciliation.js +11 -1
  50. package/dist/stream/message-reconciliation.js.map +1 -1
  51. package/dist/stream/projections/runtime.cjs +3 -2
  52. package/dist/stream/projections/runtime.cjs.map +1 -1
  53. package/dist/stream/projections/runtime.js +3 -2
  54. package/dist/stream/projections/runtime.js.map +1 -1
  55. package/dist/stream/root-message-projection.cjs +98 -53
  56. package/dist/stream/root-message-projection.cjs.map +1 -1
  57. package/dist/stream/root-message-projection.js +98 -53
  58. package/dist/stream/root-message-projection.js.map +1 -1
  59. package/dist/stream/submit-coordinator.cjs +30 -4
  60. package/dist/stream/submit-coordinator.cjs.map +1 -1
  61. package/dist/stream/submit-coordinator.js +30 -4
  62. package/dist/stream/submit-coordinator.js.map +1 -1
  63. package/dist/ui/manager.cjs +3 -2
  64. package/dist/ui/manager.cjs.map +1 -1
  65. package/dist/ui/manager.d.cts.map +1 -1
  66. package/dist/ui/manager.d.ts.map +1 -1
  67. package/dist/ui/manager.js +3 -2
  68. package/dist/ui/manager.js.map +1 -1
  69. package/package.json +3 -3
@@ -98,10 +98,16 @@ var SubmitCoordinator = class {
98
98
  #setCurrentThreadId;
99
99
  /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */
100
100
  #rememberSelfCreatedThreadId;
101
+ /** Drops a thread id from the self-created set once it's committed server-side. */
102
+ #forgetSelfCreatedThreadId;
101
103
  /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */
102
104
  #hydrate;
103
105
  /** Lazily creates / returns the active {@link ThreadStream}. */
104
106
  #ensureThread;
107
+ /** Starts the previously-deferred root pump after a self-created thread commits. */
108
+ #startDeferredRootPump;
109
+ /** Abandons a deferred root pump after a self-created dispatch fails. */
110
+ #abandonDeferredRootPump;
105
111
  /** Resolves once the controller's root subscription pump is up. */
106
112
  #waitForRootPumpReady;
107
113
  /** Resolves on the next root terminal lifecycle (or on abort). */
@@ -110,6 +116,8 @@ var SubmitCoordinator = class {
110
116
  #latestUnresolvedInterrupt;
111
117
  /** Marks an interrupt id as resolved so it isn't re-targeted. */
112
118
  #markInterruptResolved;
119
+ /** Called once at the start of every {@link submit} invocation. */
120
+ #onSubmitStart;
113
121
  /**
114
122
  * Active submission's abort controller. `undefined` between submits.
115
123
  *
@@ -126,12 +134,16 @@ var SubmitCoordinator = class {
126
134
  this.#getCurrentThreadId = params.getCurrentThreadId;
127
135
  this.#setCurrentThreadId = params.setCurrentThreadId;
128
136
  this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;
137
+ this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;
129
138
  this.#hydrate = params.hydrate;
130
139
  this.#ensureThread = params.ensureThread;
140
+ this.#startDeferredRootPump = params.startDeferredRootPump;
141
+ this.#abandonDeferredRootPump = params.abandonDeferredRootPump;
131
142
  this.#waitForRootPumpReady = params.waitForRootPumpReady;
132
143
  this.#awaitNextTerminal = params.awaitNextTerminal;
133
144
  this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;
134
145
  this.#markInterruptResolved = params.markInterruptResolved;
146
+ this.#onSubmitStart = params.onSubmitStart ?? (() => void 0);
135
147
  }
136
148
  /**
137
149
  * Submit input or a resume command to the active thread.
@@ -158,9 +170,11 @@ var SubmitCoordinator = class {
158
170
  */
159
171
  async submit(input, options) {
160
172
  if (this.#getDisposed()) return;
173
+ this.#onSubmitStart();
161
174
  const overrideThreadId = options?.threadId;
162
175
  if (overrideThreadId !== void 0 && overrideThreadId !== this.#getCurrentThreadId()) await this.#hydrate(overrideThreadId);
163
- if (this.#getCurrentThreadId() == null) {
176
+ const wasSelfCreated = this.#getCurrentThreadId() == null;
177
+ if (wasSelfCreated) {
164
178
  const threadId = (0, uuid.v7)();
165
179
  this.#setCurrentThreadId(threadId);
166
180
  this.#rememberSelfCreatedThreadId(threadId);
@@ -172,11 +186,11 @@ var SubmitCoordinator = class {
172
186
  }
173
187
  const currentThreadId = this.#getCurrentThreadId();
174
188
  if (currentThreadId == null) return;
175
- const thread = this.#ensureThread(currentThreadId);
189
+ const thread = this.#ensureThread(currentThreadId, wasSelfCreated);
176
190
  const activeThreadId = currentThreadId;
177
191
  await this.#waitForRootPumpReady();
178
192
  const strategy = options?.multitaskStrategy ?? "rollback";
179
- const hasActiveRun = this.#runAbort != null && !this.#runAbort.signal.aborted;
193
+ const hasActiveRun = !wasSelfCreated && this.#runAbort != null && !this.#runAbort.signal.aborted;
180
194
  if (hasActiveRun && strategy === "reject") throw new Error("submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.");
181
195
  if (hasActiveRun && strategy === "enqueue") {
182
196
  this.#enqueueSubmission(input, options);
@@ -215,8 +229,11 @@ var SubmitCoordinator = class {
215
229
  const commandPromise = thread.respondInput({
216
230
  namespace: target.namespace,
217
231
  interrupt_id: target.interruptId,
218
- response: resumeCommand
232
+ response: resumeCommand,
233
+ config: boundConfig,
234
+ metadata: options?.metadata
219
235
  });
236
+ commandPromise.then(() => this.#startDeferredRootPump(), () => {});
220
237
  this.#markInterruptResolved(target.interruptId);
221
238
  const first = await Promise.race([terminalPromise.then((value) => ({
222
239
  type: "terminal",
@@ -241,6 +258,15 @@ var SubmitCoordinator = class {
241
258
  forkFrom: options?.forkFrom,
242
259
  multitaskStrategy: options?.multitaskStrategy === "enqueue" ? "enqueue" : options?.multitaskStrategy
243
260
  });
261
+ commandPromise.then(() => {
262
+ this.#startDeferredRootPump();
263
+ this.#forgetSelfCreatedThreadId(activeThreadId);
264
+ }, () => {
265
+ if (wasSelfCreated) {
266
+ this.#abandonDeferredRootPump();
267
+ this.#forgetSelfCreatedThreadId(activeThreadId);
268
+ }
269
+ });
244
270
  const notifyCreated = (result) => {
245
271
  this.#options.onCreated?.({
246
272
  run_id: result.run_id,
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#hydrate","#ensureThread","#waitForRootPumpReady","#awaitNextTerminal","#latestUnresolvedInterrupt","#markInterruptResolved","#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.respondInput()` 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 StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Pointer to a pending root protocol interrupt. Used to target\n * `respondInput` for resume submissions.\n */\ninterface ResolvedInterrupt {\n interruptId: string;\n namespace: string[];\n}\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the four 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\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 /** 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: (threadId: string) => ThreadStream;\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 /** Returns the most recent unresolved root interrupt, for resumes. */\n readonly #latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n /** Marks an interrupt id as resolved so it isn't re-targeted. */\n readonly #markInterruptResolved: (interruptId: string) => 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 hydrate: (threadId?: string | null) => Promise<void>;\n ensureThread: (threadId: string) => ThreadStream;\n waitForRootPumpReady: () => Promise<void> | undefined;\n awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n markInterruptResolved: (interruptId: string) => 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.#hydrate = params.hydrate;\n this.#ensureThread = params.ensureThread;\n this.#waitForRootPumpReady = params.waitForRootPumpReady;\n this.#awaitNextTerminal = params.awaitNextTerminal;\n this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;\n this.#markInterruptResolved = params.markInterruptResolved;\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\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 if (this.#getCurrentThreadId() == null) {\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 const thread = this.#ensureThread(currentThreadId);\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 const hasActiveRun =\n this.#runAbort != null && !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\n let terminalSettled = false;\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 target = this.#latestUnresolvedInterrupt();\n if (target == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.respondInput({\n namespace: target.namespace,\n interrupt_id: target.interruptId,\n response: resumeCommand,\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 this same interrupt on the next submit.\n this.#markInterruptResolved(target.interruptId);\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 const notifyCreated = (result: { run_id?: unknown }) => {\n this.#options.onCreated?.({\n run_id: result.run_id as string,\n thread_id: activeThreadId,\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 } 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 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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"}
1
+ {"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#latestUnresolvedInterrupt","#markInterruptResolved","#onSubmitStart","#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.respondInput()` 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 StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Pointer to a pending root protocol interrupt. Used to target\n * `respondInput` for resume submissions.\n */\ninterface ResolvedInterrupt {\n interruptId: string;\n namespace: string[];\n}\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the four 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\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 /** Returns the most recent unresolved root interrupt, for resumes. */\n readonly #latestUnresolvedInterrupt: () => ResolvedInterrupt | 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\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 latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n markInterruptResolved: (interruptId: string) => void;\n onSubmitStart?: () => 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.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;\n this.#markInterruptResolved = params.markInterruptResolved;\n this.#onSubmitStart = params.onSubmitStart ?? (() => 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\n let terminalSettled = false;\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 target = this.#latestUnresolvedInterrupt();\n if (target == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.respondInput({\n namespace: target.namespace,\n interrupt_id: target.interruptId,\n response: resumeCommand,\n config: boundConfig,\n metadata: options?.metadata,\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 `latestUnresolvedInterrupt()` 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 this same interrupt on the next submit.\n this.#markInterruptResolved(target.interruptId);\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 this.#options.onCreated?.({\n run_id: result.run_id as string,\n thread_id: activeThreadId,\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 } 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 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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"}
@@ -97,10 +97,16 @@ var SubmitCoordinator = class {
97
97
  #setCurrentThreadId;
98
98
  /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */
99
99
  #rememberSelfCreatedThreadId;
100
+ /** Drops a thread id from the self-created set once it's committed server-side. */
101
+ #forgetSelfCreatedThreadId;
100
102
  /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */
101
103
  #hydrate;
102
104
  /** Lazily creates / returns the active {@link ThreadStream}. */
103
105
  #ensureThread;
106
+ /** Starts the previously-deferred root pump after a self-created thread commits. */
107
+ #startDeferredRootPump;
108
+ /** Abandons a deferred root pump after a self-created dispatch fails. */
109
+ #abandonDeferredRootPump;
104
110
  /** Resolves once the controller's root subscription pump is up. */
105
111
  #waitForRootPumpReady;
106
112
  /** Resolves on the next root terminal lifecycle (or on abort). */
@@ -109,6 +115,8 @@ var SubmitCoordinator = class {
109
115
  #latestUnresolvedInterrupt;
110
116
  /** Marks an interrupt id as resolved so it isn't re-targeted. */
111
117
  #markInterruptResolved;
118
+ /** Called once at the start of every {@link submit} invocation. */
119
+ #onSubmitStart;
112
120
  /**
113
121
  * Active submission's abort controller. `undefined` between submits.
114
122
  *
@@ -125,12 +133,16 @@ var SubmitCoordinator = class {
125
133
  this.#getCurrentThreadId = params.getCurrentThreadId;
126
134
  this.#setCurrentThreadId = params.setCurrentThreadId;
127
135
  this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;
136
+ this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;
128
137
  this.#hydrate = params.hydrate;
129
138
  this.#ensureThread = params.ensureThread;
139
+ this.#startDeferredRootPump = params.startDeferredRootPump;
140
+ this.#abandonDeferredRootPump = params.abandonDeferredRootPump;
130
141
  this.#waitForRootPumpReady = params.waitForRootPumpReady;
131
142
  this.#awaitNextTerminal = params.awaitNextTerminal;
132
143
  this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;
133
144
  this.#markInterruptResolved = params.markInterruptResolved;
145
+ this.#onSubmitStart = params.onSubmitStart ?? (() => void 0);
134
146
  }
135
147
  /**
136
148
  * Submit input or a resume command to the active thread.
@@ -157,9 +169,11 @@ var SubmitCoordinator = class {
157
169
  */
158
170
  async submit(input, options) {
159
171
  if (this.#getDisposed()) return;
172
+ this.#onSubmitStart();
160
173
  const overrideThreadId = options?.threadId;
161
174
  if (overrideThreadId !== void 0 && overrideThreadId !== this.#getCurrentThreadId()) await this.#hydrate(overrideThreadId);
162
- if (this.#getCurrentThreadId() == null) {
175
+ const wasSelfCreated = this.#getCurrentThreadId() == null;
176
+ if (wasSelfCreated) {
163
177
  const threadId = v7();
164
178
  this.#setCurrentThreadId(threadId);
165
179
  this.#rememberSelfCreatedThreadId(threadId);
@@ -171,11 +185,11 @@ var SubmitCoordinator = class {
171
185
  }
172
186
  const currentThreadId = this.#getCurrentThreadId();
173
187
  if (currentThreadId == null) return;
174
- const thread = this.#ensureThread(currentThreadId);
188
+ const thread = this.#ensureThread(currentThreadId, wasSelfCreated);
175
189
  const activeThreadId = currentThreadId;
176
190
  await this.#waitForRootPumpReady();
177
191
  const strategy = options?.multitaskStrategy ?? "rollback";
178
- const hasActiveRun = this.#runAbort != null && !this.#runAbort.signal.aborted;
192
+ const hasActiveRun = !wasSelfCreated && this.#runAbort != null && !this.#runAbort.signal.aborted;
179
193
  if (hasActiveRun && strategy === "reject") throw new Error("submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.");
180
194
  if (hasActiveRun && strategy === "enqueue") {
181
195
  this.#enqueueSubmission(input, options);
@@ -214,8 +228,11 @@ var SubmitCoordinator = class {
214
228
  const commandPromise = thread.respondInput({
215
229
  namespace: target.namespace,
216
230
  interrupt_id: target.interruptId,
217
- response: resumeCommand
231
+ response: resumeCommand,
232
+ config: boundConfig,
233
+ metadata: options?.metadata
218
234
  });
235
+ commandPromise.then(() => this.#startDeferredRootPump(), () => {});
219
236
  this.#markInterruptResolved(target.interruptId);
220
237
  const first = await Promise.race([terminalPromise.then((value) => ({
221
238
  type: "terminal",
@@ -240,6 +257,15 @@ var SubmitCoordinator = class {
240
257
  forkFrom: options?.forkFrom,
241
258
  multitaskStrategy: options?.multitaskStrategy === "enqueue" ? "enqueue" : options?.multitaskStrategy
242
259
  });
260
+ commandPromise.then(() => {
261
+ this.#startDeferredRootPump();
262
+ this.#forgetSelfCreatedThreadId(activeThreadId);
263
+ }, () => {
264
+ if (wasSelfCreated) {
265
+ this.#abandonDeferredRootPump();
266
+ this.#forgetSelfCreatedThreadId(activeThreadId);
267
+ }
268
+ });
243
269
  const notifyCreated = (result) => {
244
270
  this.#options.onCreated?.({
245
271
  run_id: result.run_id,
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.js","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#hydrate","#ensureThread","#waitForRootPumpReady","#awaitNextTerminal","#latestUnresolvedInterrupt","#markInterruptResolved","uuidv7","#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.respondInput()` 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 StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Pointer to a pending root protocol interrupt. Used to target\n * `respondInput` for resume submissions.\n */\ninterface ResolvedInterrupt {\n interruptId: string;\n namespace: string[];\n}\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the four 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\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 /** 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: (threadId: string) => ThreadStream;\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 /** Returns the most recent unresolved root interrupt, for resumes. */\n readonly #latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n /** Marks an interrupt id as resolved so it isn't re-targeted. */\n readonly #markInterruptResolved: (interruptId: string) => 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 hydrate: (threadId?: string | null) => Promise<void>;\n ensureThread: (threadId: string) => ThreadStream;\n waitForRootPumpReady: () => Promise<void> | undefined;\n awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n markInterruptResolved: (interruptId: string) => 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.#hydrate = params.hydrate;\n this.#ensureThread = params.ensureThread;\n this.#waitForRootPumpReady = params.waitForRootPumpReady;\n this.#awaitNextTerminal = params.awaitNextTerminal;\n this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;\n this.#markInterruptResolved = params.markInterruptResolved;\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\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 if (this.#getCurrentThreadId() == null) {\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 const thread = this.#ensureThread(currentThreadId);\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 const hasActiveRun =\n this.#runAbort != null && !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\n let terminalSettled = false;\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 target = this.#latestUnresolvedInterrupt();\n if (target == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.respondInput({\n namespace: target.namespace,\n interrupt_id: target.interruptId,\n response: resumeCommand,\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 this same interrupt on the next submit.\n this.#markInterruptResolved(target.interruptId);\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 const notifyCreated = (result: { run_id?: unknown }) => {\n this.#options.onCreated?.({\n run_id: result.run_id as string,\n thread_id: activeThreadId,\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 } 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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsHA,MAAa,cAA8C,OAAO,OAAO,EAAE,CAAC;;;;;;;;;;;;;;;;;;;AAoB5E,IAAa,oBAAb,MAIE;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;;;;;;;CASA;CAEA,YAAY,QAcT;AACD,QAAA,UAAgB,OAAO;AACvB,QAAA,YAAkB,OAAO;AACzB,QAAA,aAAmB,OAAO;AAC1B,QAAA,cAAoB,OAAO;AAC3B,QAAA,qBAA2B,OAAO;AAClC,QAAA,qBAA2B,OAAO;AAClC,QAAA,8BAAoC,OAAO;AAC3C,QAAA,UAAgB,OAAO;AACvB,QAAA,eAAqB,OAAO;AAC5B,QAAA,uBAA6B,OAAO;AACpC,QAAA,oBAA0B,OAAO;AACjC,QAAA,4BAAkC,OAAO;AACzC,QAAA,wBAA8B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;CA0BvC,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;AAMvC,MAAI,MAAA,oBAA0B,IAAI,MAAM;GACtC,MAAM,WAAWa,IAAQ;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;EAC7B,MAAM,SAAS,MAAA,aAAmB,gBAAgB;EAClD,MAAM,iBAAiB;AAIvB,QAAM,MAAA,sBAA4B;EAElC,MAAM,WAAW,SAAS,qBAAqB;EAC/C,MAAM,eACJ,MAAA,YAAkB,QAAQ,CAAC,MAAA,SAAe,OAAO;AACnD,MAAI,gBAAgB,aAAa,SAC/B,OAAM,IAAI,MACR,mFACD;AAEH,MAAI,gBAAgB,aAAa,WAAW;AAC1C,SAAA,kBAAwB,OAAO,QAAQ;AACvC;;AAIF,QAAA,UAAgB,OAAO;EACvB,MAAM,QAAQ,IAAI,iBAAiB;AACnC,QAAA,WAAiB;EAEjB,MAAM,gBAAgB,SAAS,SAAS;EACxC,MAAM,WAAW,kBAAkB,KAAA;AAKnC,QAAA,UAAgB,UAAU,OAAO;GAC/B,GAAG;GACH,YAAY,EAAE;GACd,WAAW,KAAA;GACX,OAAO,KAAA;GACP,WAAW;GACZ,EAAE;EAEH,MAAM,cAAc,iBAAiB,SAAS,QAAQ,gBAAgB;EAGtE,MAAM,kBAAkB,MAAA,kBAAwB,MAAM,OAAO;EAE7D,IAAI,kBAAkB;EACtB,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,IAAI;AAEJ,OAAI,UAAU;IACZ,MAAM,SAAS,MAAA,2BAAiC;AAChD,QAAI,UAAU,KACZ,OAAM,IAAI,MACR,yFACD;IAEH,MAAM,iBAAiB,OAAO,aAAa;KACzC,WAAW,OAAO;KAClB,cAAc,OAAO;KACrB,UAAU;KACX,CAAC;AAIF,UAAA,sBAA4B,OAAO,YAAY;IAC/C,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAC/B,gBAAgB,MAAM,WAAW;KAC/B,MAAM;KACN;KACD,EAAE,EACH,eAAe,YACN,EAAE,MAAM,WAAoB,IAClC,WAAW;KAAE,MAAM;KAAkB;KAAO,EAC9C,CACF,CAAC;AACF,QAAI,MAAM,SAAS,QAAS,OAAM,MAAM;AACxC,QAAI,MAAM,SAAS,YAAY;AAC7B,gBAAW,MAAM;AACjB,uBAAkB;AAGb,oBAAe,OAAO,UAAU;AACnC,UAAI,CAAC,gBAAiB,aAAY,MAAM;OACxC;;UAEC;IACL,MAAM,iBAAiB,OAAO,UAAU;KACtC,OAAO,SAAS;KAChB,QAAQ;KACR,UAAW,SAAS,YAAY,KAAA;KAChC,UAAU,SAAS;KACnB,mBACE,SAAS,sBAAsB,YAC3B,YACA,SAAS;KAChB,CAAC;IACF,MAAM,iBAAiB,WAAiC;AACtD,WAAA,QAAc,YAAY;MACxB,QAAQ,OAAO;MACf,WAAW;MACZ,CAAC;;IAEJ,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAC/B,gBAAgB,MAAM,WAAW;KAC/B,MAAM;KACN;KACD,EAAE,EACH,eAAe,MACZ,YAAY;KAAE,MAAM;KAAoB;KAAQ,IAChD,WAAW;KAAE,MAAM;KAAkB;KAAO,EAC9C,CACF,CAAC;AACF,QAAI,MAAM,SAAS,QAAS,OAAM,MAAM;AACxC,QAAI,MAAM,SAAS,UACjB,eAAc,MAAM,OAAO;SACtB;AAIL,gBAAW,MAAM;AACjB,uBAAkB;AACb,oBAAe,KAAK,cAAc,CAAC,OAAO,UAAU;AACvD,UAAI,CAAC,gBAAiB,aAAY,MAAM;OACxC;;;AAIN,gBAAa,MAAM;AACnB,qBAAkB;AAClB,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;;WAIH,OAAO;AACd,eAAY,MAAM;YACV;AAIR,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG,WAAW;IAAO,EAAE;AAC7D,OAAI,MAAA,aAAmB,MAAO,OAAA,WAAiB,KAAA;AAC/C,oBAAiB,MAAA,YAAkB,EAAE,EAAE;;;;;;;;;;;;CAa3C,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;;;;;;;;CASnB,MAAM,aAAa,IAA8B;EAC/C,MAAM,UAAU,MAAA,WAAiB,aAAa;EAC9C,MAAM,OAAO,QAAQ,QAAQ,UAAU,MAAM,OAAO,GAAG;AACvD,MAAI,KAAK,WAAW,QAAQ,OAAQ,QAAO;AAC3C,QAAA,WAAiB,eAAe,KAAK;AACrC,SAAO;;;;;CAMT,MAAM,aAA4B;AAChC,QAAA,WAAiB,eACT,YACP;;;;;;;;CASH,mBACE,OACA,SACM;EACN,MAAM,QAAyC;GAC7C,IAAIA,IAAQ;GACZ,QAAS,SAAS,KAAA;GACT;GACT,2BAAW,IAAI,MAAM;GACtB;AACD,QAAA,WAAiB,UAAU,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC;;;;;;;;;;CAW7D,cAAoB;AAClB,MAAI,MAAA,aAAmB,CAAE;AACzB,MAAI,MAAA,YAAkB,QAAQ,CAAC,MAAA,SAAe,OAAO,QAAS;EAC9D,MAAM,UAAU,MAAA,WAAiB,aAAa;AAC9C,MAAI,QAAQ,WAAW,EAAG;EAC1B,MAAM,CAAC,MAAM,GAAG,QAAQ;AACxB,QAAA,WAAiB,eAAe,KAAK;EACrC,MAAM,cAAgE;GACpE,GAAK,KAAK,WAAW,EAAE;GAIvB,mBAAmB,KAAA;GACpB;AACI,OAAK,OAAO,KAAK,QAAQ,YAAY,CAAC,YAAY,GAIrD;;;;;;;;;;;AAYN,SAAS,iBACP,QACA,UACyB;CACzB,MAAM,OACJ,UAAU,QAAQ,OAAO,WAAW,WAC/B,SACD,EAAE;CACR,MAAM,eACJ,KAAK,gBAAgB,QAAQ,OAAO,KAAK,iBAAiB,WACrD,KAAK,eACN,EAAE;AACR,QAAO;EACL,GAAG;EACH,cAAc;GACZ,GAAG;GACH,WAAW;GACZ;EACF"}
1
+ {"version":3,"file":"submit-coordinator.js","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#latestUnresolvedInterrupt","#markInterruptResolved","#onSubmitStart","uuidv7","#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.respondInput()` 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 StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Pointer to a pending root protocol interrupt. Used to target\n * `respondInput` for resume submissions.\n */\ninterface ResolvedInterrupt {\n interruptId: string;\n namespace: string[];\n}\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the four 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\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 /** Returns the most recent unresolved root interrupt, for resumes. */\n readonly #latestUnresolvedInterrupt: () => ResolvedInterrupt | 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\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 latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n markInterruptResolved: (interruptId: string) => void;\n onSubmitStart?: () => 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.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;\n this.#markInterruptResolved = params.markInterruptResolved;\n this.#onSubmitStart = params.onSubmitStart ?? (() => 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\n let terminalSettled = false;\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 target = this.#latestUnresolvedInterrupt();\n if (target == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.respondInput({\n namespace: target.namespace,\n interrupt_id: target.interruptId,\n response: resumeCommand,\n config: boundConfig,\n metadata: options?.metadata,\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 `latestUnresolvedInterrupt()` 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 this same interrupt on the next submit.\n this.#markInterruptResolved(target.interruptId);\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 this.#options.onCreated?.({\n run_id: result.run_id as string,\n thread_id: activeThreadId,\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 } 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 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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"}
@@ -454,10 +454,11 @@ var StreamManager = class {
454
454
  }
455
455
  if (this.matchEventType("messages", event, data)) {
456
456
  const [serialized, metadata] = data;
457
+ const eventCheckpointNs = namespace && require_subagents.isSubagentNamespace(namespace) ? namespace : void 0;
457
458
  const rawCheckpointNs = metadata?.langgraph_checkpoint_ns || metadata?.checkpoint_ns;
458
- const checkpointNs = typeof rawCheckpointNs === "string" ? rawCheckpointNs : void 0;
459
+ const checkpointNs = eventCheckpointNs ?? (typeof rawCheckpointNs === "string" ? rawCheckpointNs.split("|") : void 0);
459
460
  const isFromSubagent = require_subagents.isSubagentNamespace(checkpointNs);
460
- const toolCallId = isFromSubagent ? require_subagents.extractToolCallIdFromNamespace(checkpointNs?.split("|")) : void 0;
461
+ const toolCallId = isFromSubagent ? require_subagents.extractToolCallIdFromNamespace(checkpointNs) : void 0;
461
462
  if (this.filterSubagentMessages && isFromSubagent && toolCallId) {
462
463
  this.subagentManager.addMessageToSubagent(toolCallId, serialized, metadata);
463
464
  continue;