@langchain/langgraph-sdk 1.9.29 → 1.9.31

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.
@@ -155,12 +155,25 @@ var StreamController = class {
155
155
  * SSE replay re-adds historically-requested interrupts that have
156
156
  * since been resolved (no `input.responded` event exists in the
157
157
  * protocol, so the SDK has no other way to tell replay from live
158
- * for an idle thread). `null` outside the hydrate-window so
159
- * genuinely new live interrupts on an active run aren't filtered;
160
- * cleared at the start of `submit()` for the same reason.
158
+ * for an idle thread). After a command is accepted, events newer
159
+ * than {@link #interruptReplayThroughSeq} bypass the allowlist.
161
160
  */
162
161
  #hydratedActiveInterruptIds = null;
163
162
  /**
163
+ * Highest event sequence already applied when the latest run command
164
+ * was accepted. Interrupts outside the hydrate-time allowlist remain
165
+ * filtered through this sequence, while newer events are live output
166
+ * from the newly-started run and must be accepted.
167
+ */
168
+ #interruptReplayThroughSeq;
169
+ /**
170
+ * Unknown interrupt events received between local command dispatch and its
171
+ * response. The response supplies the sequence barrier needed to classify
172
+ * each event as historical replay or live output from the new run.
173
+ */
174
+ #pendingInterruptEvents = [];
175
+ #interruptCommandPending = false;
176
+ /**
164
177
  * Monotonic counter bumped at the start of each `submit()` and used
165
178
  * by {@link hydrate} to skip its post-fetch allowlist write when a
166
179
  * submit started while the state fetch was in flight. Without this
@@ -170,6 +183,14 @@ var StreamController = class {
170
183
  */
171
184
  #submitGeneration = 0;
172
185
  /**
186
+ * Monotonic counter bumped at the start of each `#teardownThread`.
187
+ * The root pump captures the generation when it starts and drops
188
+ * any event whose generation no longer matches — including queued
189
+ * events that `SubscriptionHandle.close()` still drains after the
190
+ * snapshot has already been cleared by `hydrate()`.
191
+ */
192
+ #rootPumpGeneration = 0;
193
+ /**
173
194
  * Thread ids this controller minted client-side on first `submit()`.
174
195
  * `hydrate()` skips `threads.getState()` for these — we know there
175
196
  * is nothing checkpointed yet. Cleared once dispatch commits the
@@ -294,13 +315,20 @@ var StreamController = class {
294
315
  awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
295
316
  awaitResumedRunTerminal: (signal) => this.#awaitResumedRunTerminal(signal),
296
317
  onSubmitStart: () => {
297
- this.#hydratedActiveInterruptIds = null;
298
318
  this.#submitGeneration += 1;
319
+ this.#interruptCommandPending = this.#hydratedActiveInterruptIds != null;
320
+ this.#pendingInterruptEvents = [];
299
321
  },
300
322
  onRunStart: () => this.#markLocalRunStart(),
301
- onRunCreated: (runId) => this.#notifyCreated(runId),
323
+ onRunCreated: (runId) => {
324
+ this.#advanceInterruptReplayBarrier();
325
+ this.#notifyCreated(runId);
326
+ },
302
327
  onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
303
- onRunEnd: () => this.#markLocalRunEnd(),
328
+ onRunEnd: () => {
329
+ this.#interruptCommandPending = false;
330
+ this.#markLocalRunEnd();
331
+ },
304
332
  beginOptimistic: (input) => this.#beginOptimistic(input),
305
333
  settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
306
334
  });
@@ -392,13 +420,16 @@ var StreamController = class {
392
420
  * Suspense boundary remounted against the new id suspends again.
393
421
  */
394
422
  this.#resetHydrationPromise();
395
- await this.#teardownThread();
396
423
  /**
397
- * Reset UI-facing snapshot so stale messages/values/tool-calls
398
- * from the previous thread don't bleed into the new one. The
399
- * new thread's state (if any) is then populated below via
400
- * `#applyValues`.
424
+ * Kick teardown so the synchronous abort (unbind, drop onEvent,
425
+ * close the root subscription, reset assemblers) runs before we
426
+ * touch the snapshot. Then reset immediately so the UI does not
427
+ * wait on pump/close. An interrupted thread parks the root pump
428
+ * on `waitForResume()`; awaiting teardown *before* this reset
429
+ * left `threadId` null while `messages` still showed the previous
430
+ * conversation (`useStream` fire-and-forgets `hydrate()`).
401
431
  */
432
+ const teardown = this.#teardownThread();
402
433
  this.rootStore.setState(() => ({
403
434
  ...this.#createInitialSnapshot(),
404
435
  threadId: this.#currentThreadId
@@ -409,6 +440,7 @@ var StreamController = class {
409
440
  * surprising. Mirrors the legacy `StreamOrchestrator` behaviour.
410
441
  */
411
442
  this.queueStore.setState(() => require_submit_coordinator.EMPTY_QUEUE);
443
+ await teardown;
412
444
  }
413
445
  if (this.#currentThreadId == null) {
414
446
  this.rootStore.setState((s) => ({
@@ -550,7 +582,10 @@ var StreamController = class {
550
582
  interrupts: activeInterrupts,
551
583
  interrupt: activeInterrupts[0]
552
584
  }));
553
- if (this.#submitGeneration === generationAtFetch) this.#hydratedActiveInterruptIds = activeIds;
585
+ if (this.#submitGeneration === generationAtFetch) {
586
+ this.#hydratedActiveInterruptIds = activeIds;
587
+ this.#interruptReplayThroughSeq = void 0;
588
+ }
554
589
  }
555
590
  } catch (error) {
556
591
  if (error?.status !== 404) {
@@ -997,9 +1032,11 @@ var StreamController = class {
997
1032
  config: options?.config,
998
1033
  metadata: options?.metadata
999
1034
  });
1035
+ this.#advanceInterruptReplayBarrier();
1000
1036
  this.#markInterruptResolvedInRootStore(resolved.interruptId);
1001
1037
  }, prepared?.handle);
1002
1038
  } catch (error) {
1039
+ this.#discardPendingInterruptEvents();
1003
1040
  if (this.#disposed && isAbortLikeError(error)) return;
1004
1041
  throw error;
1005
1042
  }
@@ -1066,9 +1103,11 @@ var StreamController = class {
1066
1103
  config: options?.config,
1067
1104
  metadata: options?.metadata
1068
1105
  });
1106
+ this.#advanceInterruptReplayBarrier();
1069
1107
  for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
1070
1108
  }, prepared?.handle);
1071
1109
  } catch (error) {
1110
+ this.#discardPendingInterruptEvents();
1072
1111
  if (this.#disposed && isAbortLikeError(error)) return;
1073
1112
  throw error;
1074
1113
  }
@@ -1236,6 +1275,14 @@ var StreamController = class {
1236
1275
  * Close the current thread stream and reset per-thread assembly state.
1237
1276
  */
1238
1277
  async #teardownThread() {
1278
+ /**
1279
+ * Invalidate the in-flight root pump before `close()` unparks it.
1280
+ * `SubscriptionHandle.close()` leaves queued events intact and the
1281
+ * iterator drains them before observing `closed`, so without this
1282
+ * bump a parked interrupted pump would still dispatch stale
1283
+ * `values` / `messages` into a snapshot `hydrate()` already cleared.
1284
+ */
1285
+ this.#rootPumpGeneration += 1;
1239
1286
  const thread = this.#thread;
1240
1287
  this.#thread = void 0;
1241
1288
  this.registry.bind(void 0);
@@ -1248,16 +1295,16 @@ var StreamController = class {
1248
1295
  */
1249
1296
  this.#rootEventListeners.delete(this.#lifecycleLoading.listener);
1250
1297
  this.#rootEventListeners.delete(this.#runLifecycleListener);
1251
- try {
1252
- await this.#rootSubscription?.unsubscribe();
1253
- } catch {}
1298
+ /**
1299
+ * Close first so a pump parked on `waitForResume()` (interrupted
1300
+ * run) unparks even if the unsubscribe RPC is slow. `close()` is
1301
+ * idempotent; `unsubscribe()` is then a no-op once closed.
1302
+ */
1303
+ const subscription = this.#rootSubscription;
1254
1304
  this.#rootSubscription = void 0;
1305
+ subscription?.close();
1255
1306
  this.#rootPumpReady = void 0;
1256
1307
  this.#rootPumpDeferred = false;
1257
- try {
1258
- await this.#rootPump;
1259
- } catch {}
1260
- this.#rootPump = void 0;
1261
1308
  this.#rootMessages.reset();
1262
1309
  this.#rootToolAssembler = new require_tools.ToolCallAssembler();
1263
1310
  this.#lifecycleLoading.reset();
@@ -1268,7 +1315,16 @@ var StreamController = class {
1268
1315
  this.#localRunDepth = 0;
1269
1316
  this.#messageMetadata.reset();
1270
1317
  this.#hydratedActiveInterruptIds = null;
1318
+ this.#interruptReplayThroughSeq = void 0;
1319
+ this.#discardPendingInterruptEvents();
1271
1320
  this.queueStore.setState(() => require_submit_coordinator.EMPTY_QUEUE);
1321
+ try {
1322
+ await subscription?.unsubscribe();
1323
+ } catch {}
1324
+ try {
1325
+ await this.#rootPump;
1326
+ } catch {}
1327
+ this.#rootPump = void 0;
1272
1328
  if (thread != null) {
1273
1329
  try {
1274
1330
  await thread.close();
@@ -1292,6 +1348,7 @@ var StreamController = class {
1292
1348
  */
1293
1349
  #startRootPump(thread) {
1294
1350
  if (this.#rootPump != null) return;
1351
+ const pumpGeneration = this.#rootPumpGeneration;
1295
1352
  let resolveReady;
1296
1353
  this.#rootPumpReady = new Promise((resolve) => {
1297
1354
  resolveReady = resolve;
@@ -1350,6 +1407,14 @@ var StreamController = class {
1350
1407
  resolveReady = void 0;
1351
1408
  });
1352
1409
  const subscription = await subscriptionPromise;
1410
+ if (pumpGeneration !== this.#rootPumpGeneration) {
1411
+ resolveReady?.();
1412
+ resolveReady = void 0;
1413
+ try {
1414
+ subscription.close();
1415
+ } catch {}
1416
+ return;
1417
+ }
1353
1418
  resolveReady?.();
1354
1419
  resolveReady = void 0;
1355
1420
  this.#rootSubscription = subscription;
@@ -1363,9 +1428,9 @@ var StreamController = class {
1363
1428
  * for every resumed iteration until the subscription is
1364
1429
  * permanently closed or the controller is disposed.
1365
1430
  */
1366
- while (!this.#disposed) {
1431
+ while (!this.#disposed && pumpGeneration === this.#rootPumpGeneration) {
1367
1432
  for await (const event of subscription) {
1368
- if (this.#disposed) break;
1433
+ if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
1369
1434
  /**
1370
1435
  * Resilience: isolate per-event dispatch from the pump loop.
1371
1436
  *
@@ -1397,7 +1462,7 @@ var StreamController = class {
1397
1462
  this.#onRootEvent(event);
1398
1463
  } catch {}
1399
1464
  }
1400
- if (this.#disposed) break;
1465
+ if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
1401
1466
  if (!subscription.isPaused) break;
1402
1467
  await subscription.waitForResume();
1403
1468
  }
@@ -1665,7 +1730,13 @@ var StreamController = class {
1665
1730
  const data = event.params.data;
1666
1731
  const interruptId = data?.interrupt_id;
1667
1732
  if (typeof interruptId !== "string" || this.#resolvedInterrupts.has(interruptId)) return;
1668
- if (this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId)) return;
1733
+ const eventSeq = typeof event.seq === "number" ? event.seq : void 0;
1734
+ const isUnknownInterrupt = this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId);
1735
+ if (isUnknownInterrupt && this.#interruptCommandPending) {
1736
+ this.#pendingInterruptEvents.push(event);
1737
+ return;
1738
+ }
1739
+ if (isUnknownInterrupt && (this.#interruptReplayThroughSeq == null || eventSeq != null && eventSeq <= this.#interruptReplayThroughSeq)) return;
1669
1740
  const namespace = Array.isArray(event.params.namespace) ? [...event.params.namespace] : [...ROOT_NAMESPACE];
1670
1741
  const interrupt = require_interrupts.normalizeInterruptForClient({
1671
1742
  id: interruptId,
@@ -1683,6 +1754,31 @@ var StreamController = class {
1683
1754
  });
1684
1755
  }
1685
1756
  /**
1757
+ * Convert the hydrate-time allowlist into a replay barrier after a command
1758
+ * is accepted. Built-in transports update `lastAppliedThroughSeq` from the
1759
+ * command response before resolving `submitRun` / `respondInput`.
1760
+ */
1761
+ #advanceInterruptReplayBarrier() {
1762
+ if (this.#hydratedActiveInterruptIds == null) {
1763
+ this.#discardPendingInterruptEvents();
1764
+ return;
1765
+ }
1766
+ const throughSeq = this.#thread?.ordering?.lastAppliedThroughSeq;
1767
+ if (typeof throughSeq === "number") this.#interruptReplayThroughSeq = throughSeq;
1768
+ else {
1769
+ this.#hydratedActiveInterruptIds = null;
1770
+ this.#interruptReplayThroughSeq = void 0;
1771
+ }
1772
+ this.#interruptCommandPending = false;
1773
+ const pendingEvents = this.#pendingInterruptEvents;
1774
+ this.#pendingInterruptEvents = [];
1775
+ for (const event of pendingEvents) this.#recordInterrupt(event);
1776
+ }
1777
+ #discardPendingInterruptEvents() {
1778
+ this.#interruptCommandPending = false;
1779
+ this.#pendingInterruptEvents = [];
1780
+ }
1781
+ /**
1686
1782
  * Resolve the protocol namespace for a targeted resume.
1687
1783
  *
1688
1784
  * Prefer an explicit caller-supplied namespace, then the matching