@langchain/langgraph-sdk 1.9.28 → 1.9.30

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 (49) hide show
  1. package/dist/client/stream/types.d.cts +4 -4
  2. package/dist/client/stream/types.d.ts +4 -4
  3. package/dist/schema.d.cts +7 -0
  4. package/dist/schema.d.cts.map +1 -1
  5. package/dist/schema.d.ts +7 -0
  6. package/dist/schema.d.ts.map +1 -1
  7. package/dist/stream/controller.cjs +135 -71
  8. package/dist/stream/controller.cjs.map +1 -1
  9. package/dist/stream/controller.d.cts +12 -23
  10. package/dist/stream/controller.d.cts.map +1 -1
  11. package/dist/stream/controller.d.ts +12 -23
  12. package/dist/stream/controller.d.ts.map +1 -1
  13. package/dist/stream/controller.js +135 -71
  14. package/dist/stream/controller.js.map +1 -1
  15. package/dist/stream/lifecycle-loading-tracker.cjs +12 -2
  16. package/dist/stream/lifecycle-loading-tracker.cjs.map +1 -1
  17. package/dist/stream/lifecycle-loading-tracker.js +12 -2
  18. package/dist/stream/lifecycle-loading-tracker.js.map +1 -1
  19. package/dist/stream/message-reconciliation.cjs +80 -4
  20. package/dist/stream/message-reconciliation.cjs.map +1 -1
  21. package/dist/stream/message-reconciliation.js +80 -4
  22. package/dist/stream/message-reconciliation.js.map +1 -1
  23. package/dist/stream/projections/channel.cjs +4 -6
  24. package/dist/stream/projections/channel.cjs.map +1 -1
  25. package/dist/stream/projections/channel.d.cts.map +1 -1
  26. package/dist/stream/projections/channel.d.ts.map +1 -1
  27. package/dist/stream/projections/channel.js +4 -6
  28. package/dist/stream/projections/channel.js.map +1 -1
  29. package/dist/stream/projections/messages.cjs +1 -1
  30. package/dist/stream/projections/messages.cjs.map +1 -1
  31. package/dist/stream/projections/messages.js +2 -2
  32. package/dist/stream/projections/messages.js.map +1 -1
  33. package/dist/stream/root-message-projection.cjs +12 -6
  34. package/dist/stream/root-message-projection.cjs.map +1 -1
  35. package/dist/stream/root-message-projection.js +13 -7
  36. package/dist/stream/root-message-projection.js.map +1 -1
  37. package/dist/stream/submit-coordinator.cjs +6 -5
  38. package/dist/stream/submit-coordinator.cjs.map +1 -1
  39. package/dist/stream/submit-coordinator.js +6 -5
  40. package/dist/stream/submit-coordinator.js.map +1 -1
  41. package/dist/stream/types.d.cts +3 -3
  42. package/dist/stream/types.d.ts +3 -3
  43. package/dist/utils/async_caller.cjs +1 -1
  44. package/dist/utils/async_caller.cjs.map +1 -1
  45. package/dist/utils/async_caller.d.cts.map +1 -1
  46. package/dist/utils/async_caller.d.ts.map +1 -1
  47. package/dist/utils/async_caller.js +1 -1
  48. package/dist/utils/async_caller.js.map +1 -1
  49. package/package.json +7 -7
@@ -120,14 +120,15 @@ declare class StreamController<StateType extends object = Record<string, unknown
120
120
  * oldest and picks the first entry whose `interruptId` has not already
121
121
  * been resolved by a prior `respond()` call. That entry may be at the
122
122
  * root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
123
- * This is **not** the same as {@link RootSnapshot.interrupts
124
- * `rootStore.interrupts[0]`} / framework `stream.interrupt`, which only
125
- * mirrors root-namespace interrupts for UI convenience.
123
+ * {@link RootSnapshot.interrupts `rootStore.interrupts`} /
124
+ * framework `stream.interrupts` mirrors the same pending interrupts
125
+ * (including nested namespaces) for UI rendering.
126
126
  *
127
127
  * Omitting `interruptId` is fine when exactly one interrupt is pending.
128
128
  * When several can be active (parallel subagents, fan-out, nested
129
- * graphs), pass an explicit `interruptId` (and `namespace` for subgraph
130
- * interrupts) so you resume the interrupt the user acted on.
129
+ * graphs), pass an explicit `interruptId` so you resume the interrupt
130
+ * the user acted on. `namespace` is resolved automatically from
131
+ * `thread.interrupts` / `stream.interrupts` when omitted.
131
132
  *
132
133
  * To resume several interrupts pending at the same checkpoint in one
133
134
  * command, use {@link respondAll} — sequential single `respond()` calls
@@ -135,9 +136,8 @@ declare class StreamController<StateType extends object = Record<string, unknown
135
136
  * others with no interrupted run to respond to.
136
137
  *
137
138
  * The server validates `namespace` against the pending interrupt. Root
138
- * interrupts use `namespace: []` (the default when `namespace` is
139
- * omitted). Subgraph interrupts require the exact tuple from
140
- * `getThread()?.interrupts` — see the example below.
139
+ * interrupts use `namespace: []`. Subgraph interrupts carry the exact
140
+ * tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
141
141
  *
142
142
  * @param response - Payload sent back to the interrupted namespace.
143
143
  * @param options - Optional target (`interruptId` / `namespace`) and
@@ -158,27 +158,16 @@ declare class StreamController<StateType extends object = Record<string, unknown
158
158
  * );
159
159
  * ```
160
160
  *
161
- * @example Multiple root interrupts — target by id
161
+ * @example Multiple / nested interrupts — target by id
162
162
  * ```tsx
163
163
  * for (const intr of stream.interrupts) {
164
164
  * await stream.respond(decide(intr.value), { interruptId: intr.id! });
165
165
  * }
166
166
  * ```
167
167
  *
168
- * @example Subgraph interrupt — read `namespace` from the thread stream
169
- * ```tsx
170
- * const thread = stream.getThread();
171
- * for (const entry of thread?.interrupts ?? []) {
172
- * await stream.respond(buildResponse(entry.payload), {
173
- * interruptId: entry.interruptId,
174
- * namespace: entry.namespace,
175
- * });
176
- * }
177
- * ```
178
- *
179
- * Each {@link InterruptPayload} on `thread.interrupts` mirrors an
180
- * `input.requested` event: `{ interruptId, payload, namespace }`.
181
- * Nested interrupts may appear here but not on `stream.interrupts`.
168
+ * Each {@link InterruptPayload} on `thread.interrupts` and each
169
+ * {@link Interrupt} on `stream.interrupts` mirrors an
170
+ * `input.requested` event with its protocol `namespace`.
182
171
  */
183
172
  respond(response: unknown, options?: StreamRespondOptions<ConfigurableType>): Promise<void>;
184
173
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.cts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;AA6KA;;cAAa,kBAAA,WAA6B,OAAA;;AAkC1C;;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAsH0B;;;;;EAA7C,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EA0vBF;;;;;;;EAAA,IA/oBvC,gBAAA,CAAA,GAAoB,OAAA;EA+zBS;;;;;;;;EAtwB3B,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAm/BpB;;;;;;;;;;;;;;EAlqBf,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EAhnB5B;;;;;;;;EAm3BlB,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAn3B4B;;;;;;EA63BzB,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EA33BtB;;;;EA+4Bb,UAAA,CAAA,GAAc,OAAA;EA9qBhB;;;;;;;;;;;EA8uBE,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FW;;;EAsGrC,UAAA,CAAA,GAAc,OAAA;EA3FC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmKf,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA4DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
1
+ {"version":3,"file":"controller.d.cts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;AA6KA;;cAAa,kBAAA,WAA6B,OAAA;;AAkC1C;;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAwI0B;;;;;EAA7C,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EA0yBF;;;;;;;EAAA,IA3rBvC,gBAAA,CAAA,GAAoB,OAAA;EAg2BS;;;;;;;;EAvyB3B,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAqhCpB;;;;;;;;;;;;;;EAzpBf,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EAjrB5B;;;;;;;;EAq7BlB,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAr7B4B;;;;;;EA+7BzB,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EA77BtB;;;;EAi9Bb,UAAA,CAAA,GAAc,OAAA;EA1tBhB;;;;;;;;;;;EA0xBE,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FW;;;EAsGrC,UAAA,CAAA,GAAc,OAAA;EA3FC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwJf,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA0DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
@@ -120,14 +120,15 @@ declare class StreamController<StateType extends object = Record<string, unknown
120
120
  * oldest and picks the first entry whose `interruptId` has not already
121
121
  * been resolved by a prior `respond()` call. That entry may be at the
122
122
  * root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
123
- * This is **not** the same as {@link RootSnapshot.interrupts
124
- * `rootStore.interrupts[0]`} / framework `stream.interrupt`, which only
125
- * mirrors root-namespace interrupts for UI convenience.
123
+ * {@link RootSnapshot.interrupts `rootStore.interrupts`} /
124
+ * framework `stream.interrupts` mirrors the same pending interrupts
125
+ * (including nested namespaces) for UI rendering.
126
126
  *
127
127
  * Omitting `interruptId` is fine when exactly one interrupt is pending.
128
128
  * When several can be active (parallel subagents, fan-out, nested
129
- * graphs), pass an explicit `interruptId` (and `namespace` for subgraph
130
- * interrupts) so you resume the interrupt the user acted on.
129
+ * graphs), pass an explicit `interruptId` so you resume the interrupt
130
+ * the user acted on. `namespace` is resolved automatically from
131
+ * `thread.interrupts` / `stream.interrupts` when omitted.
131
132
  *
132
133
  * To resume several interrupts pending at the same checkpoint in one
133
134
  * command, use {@link respondAll} — sequential single `respond()` calls
@@ -135,9 +136,8 @@ declare class StreamController<StateType extends object = Record<string, unknown
135
136
  * others with no interrupted run to respond to.
136
137
  *
137
138
  * The server validates `namespace` against the pending interrupt. Root
138
- * interrupts use `namespace: []` (the default when `namespace` is
139
- * omitted). Subgraph interrupts require the exact tuple from
140
- * `getThread()?.interrupts` — see the example below.
139
+ * interrupts use `namespace: []`. Subgraph interrupts carry the exact
140
+ * tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
141
141
  *
142
142
  * @param response - Payload sent back to the interrupted namespace.
143
143
  * @param options - Optional target (`interruptId` / `namespace`) and
@@ -158,27 +158,16 @@ declare class StreamController<StateType extends object = Record<string, unknown
158
158
  * );
159
159
  * ```
160
160
  *
161
- * @example Multiple root interrupts — target by id
161
+ * @example Multiple / nested interrupts — target by id
162
162
  * ```tsx
163
163
  * for (const intr of stream.interrupts) {
164
164
  * await stream.respond(decide(intr.value), { interruptId: intr.id! });
165
165
  * }
166
166
  * ```
167
167
  *
168
- * @example Subgraph interrupt — read `namespace` from the thread stream
169
- * ```tsx
170
- * const thread = stream.getThread();
171
- * for (const entry of thread?.interrupts ?? []) {
172
- * await stream.respond(buildResponse(entry.payload), {
173
- * interruptId: entry.interruptId,
174
- * namespace: entry.namespace,
175
- * });
176
- * }
177
- * ```
178
- *
179
- * Each {@link InterruptPayload} on `thread.interrupts` mirrors an
180
- * `input.requested` event: `{ interruptId, payload, namespace }`.
181
- * Nested interrupts may appear here but not on `stream.interrupts`.
168
+ * Each {@link InterruptPayload} on `thread.interrupts` and each
169
+ * {@link Interrupt} on `stream.interrupts` mirrors an
170
+ * `input.requested` event with its protocol `namespace`.
182
171
  */
183
172
  respond(response: unknown, options?: StreamRespondOptions<ConfigurableType>): Promise<void>;
184
173
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;;AA6KA;cAAa,kBAAA,WAA6B,OAAA;;;AAkC1C;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAAA;;;;;EAsHnB,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EA0vBb;;;;;;;EAAA,IA/oB5B,gBAAA,CAAA,GAAoB,OAAA;EAqvBJ;;;;;;;;EA5rBd,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAu+B5B;;;;;;;;;;;;;;EAtpBP,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EAhnB3C;;;;;;;;EAm3BH,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAn3BM;;;;;;EA63BH,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EA33BhC;;;;EA+4BH,UAAA,CAAA,GAAc,OAAA;EAzxBR;;;;;;;;;;;EAy1BN,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FA;;;EAsG1B,UAAA,CAAA,GAAc,OAAA;EA3Fd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmKA,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA4DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
1
+ {"version":3,"file":"controller.d.ts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;;AA6KA;cAAa,kBAAA,WAA6B,OAAA;;;AAkC1C;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAAA;;;;;EAwInB,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EA0yBb;;;;;;;EAAA,IA3rB5B,gBAAA,CAAA,GAAoB,OAAA;EAiyBJ;;;;;;;;EAxuBd,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAygC5B;;;;;;;;;;;;;;EA7oBP,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EAjrB3C;;;;;;;;EAq7BH,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAr7BM;;;;;;EA+7BH,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EA77BhC;;;;EAi9BH,UAAA,CAAA,GAAc,OAAA;EAz0BR;;;;;;;;;;;EAy4BN,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FA;;;EAsG1B,UAAA,CAAA,GAAc,OAAA;EA3Fd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwJA,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA0DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
@@ -169,13 +169,31 @@ var StreamController = class {
169
169
  */
170
170
  #submitGeneration = 0;
171
171
  /**
172
- * Thread ids we minted client-side on first `submit()`. Keeping them
173
- * here lets `hydrate()` skip the `threads.getState()` round-trip —
174
- * we know there is nothing checkpointed server-side yet (and the
175
- * request would 404 and surface a spurious error to the UI).
172
+ * Monotonic counter bumped at the start of each `#teardownThread`.
173
+ * The root pump captures the generation when it starts and drops
174
+ * any event whose generation no longer matches — including queued
175
+ * events that `SubscriptionHandle.close()` still drains after the
176
+ * snapshot has already been cleared by `hydrate()`.
177
+ */
178
+ #rootPumpGeneration = 0;
179
+ /**
180
+ * Thread ids this controller minted client-side on first `submit()`.
181
+ * `hydrate()` skips `threads.getState()` for these — we know there
182
+ * is nothing checkpointed yet. Cleared once dispatch commits the
183
+ * thread server-side.
176
184
  */
177
185
  #selfCreatedThreadIds = /* @__PURE__ */ new Set();
178
186
  /**
187
+ * Caller-supplied thread ids that 404'd on hydrate. Unlike
188
+ * {@link #selfCreatedThreadIds}, these are revalidated on every
189
+ * hydrate: another client may create the same id between visits.
190
+ * While marked missing, `submit()` still defers the root SSE pump
191
+ * until `run.start` / `commands` commits the row — joining
192
+ * `/stream/events` beforehand leaves a dead subscription on
193
+ * langgraph_api's in-mem runtime.
194
+ */
195
+ #missingThreadIds = /* @__PURE__ */ new Set();
196
+ /**
179
197
  * In-flight per-subagent namespace resolutions, keyed by tool-call
180
198
  * id. De-dupes concurrent {@link resolveSubagentNamespace} calls so
181
199
  * re-renders / multiple consumers of the same subagent don't issue
@@ -270,12 +288,14 @@ var StreamController = class {
270
288
  rememberSelfCreatedThreadId: (threadId) => {
271
289
  this.#selfCreatedThreadIds.add(threadId);
272
290
  },
291
+ isSelfCreatedThreadId: (threadId) => this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId),
273
292
  hydrate: (threadId) => this.hydrate(threadId),
274
293
  ensureThread: (threadId, deferRootPump) => this.#ensureThread(threadId, deferRootPump),
275
294
  startDeferredRootPump: () => this.#startDeferredRootPump(),
276
295
  abandonDeferredRootPump: () => this.#abandonDeferredRootPump(),
277
296
  forgetSelfCreatedThreadId: (threadId) => {
278
297
  this.#selfCreatedThreadIds.delete(threadId);
298
+ this.#missingThreadIds.delete(threadId);
279
299
  },
280
300
  waitForRootPumpReady: () => this.#rootPumpReady,
281
301
  awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
@@ -379,13 +399,16 @@ var StreamController = class {
379
399
  * Suspense boundary remounted against the new id suspends again.
380
400
  */
381
401
  this.#resetHydrationPromise();
382
- await this.#teardownThread();
383
402
  /**
384
- * Reset UI-facing snapshot so stale messages/values/tool-calls
385
- * from the previous thread don't bleed into the new one. The
386
- * new thread's state (if any) is then populated below via
387
- * `#applyValues`.
403
+ * Kick teardown so the synchronous abort (unbind, drop onEvent,
404
+ * close the root subscription, reset assemblers) runs before we
405
+ * touch the snapshot. Then reset immediately so the UI does not
406
+ * wait on pump/close. An interrupted thread parks the root pump
407
+ * on `waitForResume()`; awaiting teardown *before* this reset
408
+ * left `threadId` null while `messages` still showed the previous
409
+ * conversation (`useStream` fire-and-forgets `hydrate()`).
388
410
  */
411
+ const teardown = this.#teardownThread();
389
412
  this.rootStore.setState(() => ({
390
413
  ...this.#createInitialSnapshot(),
391
414
  threadId: this.#currentThreadId
@@ -396,6 +419,7 @@ var StreamController = class {
396
419
  * surprising. Mirrors the legacy `StreamOrchestrator` behaviour.
397
420
  */
398
421
  this.queueStore.setState(() => EMPTY_QUEUE);
422
+ await teardown;
399
423
  }
400
424
  if (this.#currentThreadId == null) {
401
425
  this.rootStore.setState((s) => ({
@@ -426,12 +450,14 @@ var StreamController = class {
426
450
  const hydratedThreadId = this.#currentThreadId;
427
451
  let hydrationError;
428
452
  let threadExists = false;
453
+ let threadMissing = false;
429
454
  let threadActive = true;
430
455
  try {
431
456
  const state = await this.#fetchHydrationState();
432
457
  if (this.#disposed || this.#currentThreadId !== hydratedThreadId) return;
433
458
  threadExists = state != null;
434
459
  threadActive = isThreadStateActive(state);
460
+ this.#missingThreadIds.delete(hydratedThreadId);
435
461
  if (state?.values != null) {
436
462
  /**
437
463
  * `threads.getState()` returns the legacy `ThreadState` shape
@@ -516,6 +542,8 @@ var StreamController = class {
516
542
  const activeIds = /* @__PURE__ */ new Set();
517
543
  for (const task of state.tasks) {
518
544
  if (!Array.isArray(task?.interrupts)) continue;
545
+ const checkpointNs = task?.checkpoint?.checkpoint_ns;
546
+ const namespace = typeof checkpointNs === "string" && checkpointNs.length > 0 ? checkpointNs.split("|").filter((segment) => segment.length > 0) : [...ROOT_NAMESPACE];
519
547
  for (const interrupt of task.interrupts) {
520
548
  const typed = interrupt;
521
549
  const id = typed?.id;
@@ -523,7 +551,8 @@ var StreamController = class {
523
551
  activeIds.add(id);
524
552
  activeInterrupts.push(normalizeInterruptForClient({
525
553
  id,
526
- value: typed?.value
554
+ value: typed?.value,
555
+ namespace: Array.isArray(typed?.namespace) ? [...typed.namespace] : namespace
527
556
  }));
528
557
  }
529
558
  }
@@ -541,7 +570,7 @@ var StreamController = class {
541
570
  ...s,
542
571
  error
543
572
  }));
544
- }
573
+ } else threadMissing = true;
545
574
  } finally {
546
575
  this.rootStore.setState((s) => ({
547
576
  ...s,
@@ -551,6 +580,20 @@ var StreamController = class {
551
580
  else this.#resolveHydration();
552
581
  }
553
582
  /**
583
+ * Missing threads must not open `/stream/events`. The in-mem
584
+ * runtime accepts the join against a not-yet-created id but the
585
+ * subscription never attaches to the live channel — the first run
586
+ * then delivers 0 bytes until idle reconnect. Park the pump until
587
+ * `submit()`'s `run.start` creates the row (see
588
+ * {@link #startDeferredRootPump}). Tracked in `#missingThreadIds`
589
+ * (not `#selfCreatedThreadIds`) so a later hydrate revalidates —
590
+ * another client may create the same id between visits.
591
+ */
592
+ if (threadMissing) {
593
+ this.#missingThreadIds.add(hydratedThreadId);
594
+ return;
595
+ }
596
+ /**
554
597
  * Open the shared subscription on mount so in-flight server-side
555
598
  * runs are observed even when no local `submit()` is active — BUT
556
599
  * only when the thread is actually active (see
@@ -687,7 +730,7 @@ var StreamController = class {
687
730
  */
688
731
  async #trySeedProjectionFromHistory(params) {
689
732
  const threadId = this.#currentThreadId;
690
- if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId)) return false;
733
+ if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId)) return false;
691
734
  if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
692
735
  if (this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
693
736
  const seed = await this.#getScopedHistorySeed(threadId, params.namespace);
@@ -895,14 +938,15 @@ var StreamController = class {
895
938
  * oldest and picks the first entry whose `interruptId` has not already
896
939
  * been resolved by a prior `respond()` call. That entry may be at the
897
940
  * root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
898
- * This is **not** the same as {@link RootSnapshot.interrupts
899
- * `rootStore.interrupts[0]`} / framework `stream.interrupt`, which only
900
- * mirrors root-namespace interrupts for UI convenience.
941
+ * {@link RootSnapshot.interrupts `rootStore.interrupts`} /
942
+ * framework `stream.interrupts` mirrors the same pending interrupts
943
+ * (including nested namespaces) for UI rendering.
901
944
  *
902
945
  * Omitting `interruptId` is fine when exactly one interrupt is pending.
903
946
  * When several can be active (parallel subagents, fan-out, nested
904
- * graphs), pass an explicit `interruptId` (and `namespace` for subgraph
905
- * interrupts) so you resume the interrupt the user acted on.
947
+ * graphs), pass an explicit `interruptId` so you resume the interrupt
948
+ * the user acted on. `namespace` is resolved automatically from
949
+ * `thread.interrupts` / `stream.interrupts` when omitted.
906
950
  *
907
951
  * To resume several interrupts pending at the same checkpoint in one
908
952
  * command, use {@link respondAll} — sequential single `respond()` calls
@@ -910,9 +954,8 @@ var StreamController = class {
910
954
  * others with no interrupted run to respond to.
911
955
  *
912
956
  * The server validates `namespace` against the pending interrupt. Root
913
- * interrupts use `namespace: []` (the default when `namespace` is
914
- * omitted). Subgraph interrupts require the exact tuple from
915
- * `getThread()?.interrupts` — see the example below.
957
+ * interrupts use `namespace: []`. Subgraph interrupts carry the exact
958
+ * tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
916
959
  *
917
960
  * @param response - Payload sent back to the interrupted namespace.
918
961
  * @param options - Optional target (`interruptId` / `namespace`) and
@@ -933,33 +976,22 @@ var StreamController = class {
933
976
  * );
934
977
  * ```
935
978
  *
936
- * @example Multiple root interrupts — target by id
979
+ * @example Multiple / nested interrupts — target by id
937
980
  * ```tsx
938
981
  * for (const intr of stream.interrupts) {
939
982
  * await stream.respond(decide(intr.value), { interruptId: intr.id! });
940
983
  * }
941
984
  * ```
942
985
  *
943
- * @example Subgraph interrupt — read `namespace` from the thread stream
944
- * ```tsx
945
- * const thread = stream.getThread();
946
- * for (const entry of thread?.interrupts ?? []) {
947
- * await stream.respond(buildResponse(entry.payload), {
948
- * interruptId: entry.interruptId,
949
- * namespace: entry.namespace,
950
- * });
951
- * }
952
- * ```
953
- *
954
- * Each {@link InterruptPayload} on `thread.interrupts` mirrors an
955
- * `input.requested` event: `{ interruptId, payload, namespace }`.
956
- * Nested interrupts may appear here but not on `stream.interrupts`.
986
+ * Each {@link InterruptPayload} on `thread.interrupts` and each
987
+ * {@link Interrupt} on `stream.interrupts` mirrors an
988
+ * `input.requested` event with its protocol `namespace`.
957
989
  */
958
990
  async respond(response, options) {
959
991
  if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
960
992
  const resolved = options?.interruptId != null ? {
961
993
  interruptId: options.interruptId,
962
- namespace: options.namespace ?? [...ROOT_NAMESPACE]
994
+ namespace: this.#resolveNamespaceForInterruptId(options.interruptId, options.namespace)
963
995
  } : this.#resolveInterruptForResume();
964
996
  if (resolved == null) throw new Error("No pending interrupt to respond to.");
965
997
  const thread = this.#thread;
@@ -1029,11 +1061,10 @@ var StreamController = class {
1029
1061
  const entries = Object.entries(responsesById);
1030
1062
  if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
1031
1063
  const thread = this.#thread;
1032
- const pending = thread.interrupts;
1033
1064
  const responses = entries.map(([interruptId, response]) => ({
1034
1065
  interrupt_id: interruptId,
1035
1066
  response: normalizeHitlResponseForServer(response),
1036
- namespace: pending.find((entry) => entry.interruptId === interruptId)?.namespace ?? [...ROOT_NAMESPACE]
1067
+ namespace: this.#resolveNamespaceForInterruptId(interruptId)
1037
1068
  }));
1038
1069
  const prepared = options?.update != null ? this.#beginOptimistic(options.update) : void 0;
1039
1070
  const dispatchUpdate = this.#resolveDispatchUpdate(options?.update, prepared);
@@ -1200,16 +1231,11 @@ var StreamController = class {
1200
1231
  * Without this, the controller would be wedged in a state where:
1201
1232
  * - `#thread` is wired but no content pump is open
1202
1233
  * - `#rootPumpDeferred` stays `true`
1203
- * - `selfCreatedThreadIds` still holds the id
1204
1234
  *
1205
- * A retry submit on the same controller would see
1206
- * `wasSelfCreated=false` (because `currentThreadId` is no longer
1207
- * null), `#ensureThread(id, false)` would early-return because
1208
- * `#thread != null`, and the pump would never start. The thread
1209
- * would have an id committed to the URL but no live subscription.
1210
- *
1211
- * Tearing down `#thread` so the next submit re-runs `#ensureThread`
1212
- * from scratch is the simplest recovery — the failed dispatch
1235
+ * A retry submit must re-run `#ensureThread` from scratch (with
1236
+ * `deferRootPump` still true — the id stays in
1237
+ * `#selfCreatedThreadIds` until a successful dispatch). Tearing
1238
+ * down `#thread` is the simplest recovery; the failed dispatch
1213
1239
  * means there was nothing to subscribe to anyway.
1214
1240
  */
1215
1241
  #abandonDeferredRootPump() {
@@ -1221,6 +1247,14 @@ var StreamController = class {
1221
1247
  * Close the current thread stream and reset per-thread assembly state.
1222
1248
  */
1223
1249
  async #teardownThread() {
1250
+ /**
1251
+ * Invalidate the in-flight root pump before `close()` unparks it.
1252
+ * `SubscriptionHandle.close()` leaves queued events intact and the
1253
+ * iterator drains them before observing `closed`, so without this
1254
+ * bump a parked interrupted pump would still dispatch stale
1255
+ * `values` / `messages` into a snapshot `hydrate()` already cleared.
1256
+ */
1257
+ this.#rootPumpGeneration += 1;
1224
1258
  const thread = this.#thread;
1225
1259
  this.#thread = void 0;
1226
1260
  this.registry.bind(void 0);
@@ -1233,16 +1267,16 @@ var StreamController = class {
1233
1267
  */
1234
1268
  this.#rootEventListeners.delete(this.#lifecycleLoading.listener);
1235
1269
  this.#rootEventListeners.delete(this.#runLifecycleListener);
1236
- try {
1237
- await this.#rootSubscription?.unsubscribe();
1238
- } catch {}
1270
+ /**
1271
+ * Close first so a pump parked on `waitForResume()` (interrupted
1272
+ * run) unparks even if the unsubscribe RPC is slow. `close()` is
1273
+ * idempotent; `unsubscribe()` is then a no-op once closed.
1274
+ */
1275
+ const subscription = this.#rootSubscription;
1239
1276
  this.#rootSubscription = void 0;
1277
+ subscription?.close();
1240
1278
  this.#rootPumpReady = void 0;
1241
1279
  this.#rootPumpDeferred = false;
1242
- try {
1243
- await this.#rootPump;
1244
- } catch {}
1245
- this.#rootPump = void 0;
1246
1280
  this.#rootMessages.reset();
1247
1281
  this.#rootToolAssembler = new ToolCallAssembler();
1248
1282
  this.#lifecycleLoading.reset();
@@ -1254,6 +1288,13 @@ var StreamController = class {
1254
1288
  this.#messageMetadata.reset();
1255
1289
  this.#hydratedActiveInterruptIds = null;
1256
1290
  this.queueStore.setState(() => EMPTY_QUEUE);
1291
+ try {
1292
+ await subscription?.unsubscribe();
1293
+ } catch {}
1294
+ try {
1295
+ await this.#rootPump;
1296
+ } catch {}
1297
+ this.#rootPump = void 0;
1257
1298
  if (thread != null) {
1258
1299
  try {
1259
1300
  await thread.close();
@@ -1277,6 +1318,7 @@ var StreamController = class {
1277
1318
  */
1278
1319
  #startRootPump(thread) {
1279
1320
  if (this.#rootPump != null) return;
1321
+ const pumpGeneration = this.#rootPumpGeneration;
1280
1322
  let resolveReady;
1281
1323
  this.#rootPumpReady = new Promise((resolve) => {
1282
1324
  resolveReady = resolve;
@@ -1335,6 +1377,14 @@ var StreamController = class {
1335
1377
  resolveReady = void 0;
1336
1378
  });
1337
1379
  const subscription = await subscriptionPromise;
1380
+ if (pumpGeneration !== this.#rootPumpGeneration) {
1381
+ resolveReady?.();
1382
+ resolveReady = void 0;
1383
+ try {
1384
+ subscription.close();
1385
+ } catch {}
1386
+ return;
1387
+ }
1338
1388
  resolveReady?.();
1339
1389
  resolveReady = void 0;
1340
1390
  this.#rootSubscription = subscription;
@@ -1348,9 +1398,9 @@ var StreamController = class {
1348
1398
  * for every resumed iteration until the subscription is
1349
1399
  * permanently closed or the controller is disposed.
1350
1400
  */
1351
- while (!this.#disposed) {
1401
+ while (!this.#disposed && pumpGeneration === this.#rootPumpGeneration) {
1352
1402
  for await (const event of subscription) {
1353
- if (this.#disposed) break;
1403
+ if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
1354
1404
  /**
1355
1405
  * Resilience: isolate per-event dispatch from the pump loop.
1356
1406
  *
@@ -1382,7 +1432,7 @@ var StreamController = class {
1382
1432
  this.#onRootEvent(event);
1383
1433
  } catch {}
1384
1434
  }
1385
- if (this.#disposed) break;
1435
+ if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
1386
1436
  if (!subscription.isPaused) break;
1387
1437
  await subscription.waitForResume();
1388
1438
  }
@@ -1412,16 +1462,14 @@ var StreamController = class {
1412
1462
  this.#subgraphs.push(event);
1413
1463
  this.#lifecycleLoading.handle(event);
1414
1464
  /**
1415
- * Nested `input.requested` events (HITL inside a subagent /
1416
- * subgraph) are not observable via the narrow content pump. The
1417
- * `ThreadStream` itself already records them into
1418
- * `thread.interrupts`, which `#latestUnresolvedInterrupt()`
1419
- * consults — so HITL respond() works for any depth. Root-level
1420
- * interrupts are also mirrored into `rootStore.interrupts` here so
1421
- * UI state does not depend on the narrower content pump being the
1422
- * first consumer to see the event.
1465
+ * `input.requested` events (including HITL inside a subagent /
1466
+ * subgraph) are not always observable via the narrow content pump.
1467
+ * Mirror every namespace into `rootStore.interrupts` here so UI
1468
+ * state does not depend on the content pump being the first
1469
+ * consumer. `ThreadStream.interrupts` is updated in parallel for
1470
+ * resume targeting.
1423
1471
  */
1424
- this.#recordRootInterrupt(event);
1472
+ this.#recordInterrupt(event);
1425
1473
  }
1426
1474
  /**
1427
1475
  * Process one root-pump event and update all root projections.
@@ -1514,7 +1562,7 @@ var StreamController = class {
1514
1562
  return;
1515
1563
  }
1516
1564
  if (event.method === "input.requested") {
1517
- this.#recordRootInterrupt(event);
1565
+ this.#recordInterrupt(event);
1518
1566
  return;
1519
1567
  }
1520
1568
  if (event.method === "lifecycle") event.params.data;
@@ -1642,21 +1690,22 @@ var StreamController = class {
1642
1690
  }
1643
1691
  }
1644
1692
  /**
1645
- * Mirror root protocol interrupts into the root snapshot.
1693
+ * Mirror protocol interrupts (any namespace) into the root snapshot.
1646
1694
  *
1647
1695
  * This can be called from both the wildcard lifecycle/input watcher and the
1648
1696
  * root content pump. Store-level dedup keeps the user-facing list stable.
1649
1697
  */
1650
- #recordRootInterrupt(event) {
1698
+ #recordInterrupt(event) {
1651
1699
  if (event.method !== "input.requested") return;
1652
- if (!isRootNamespace(event.params.namespace)) return;
1653
1700
  const data = event.params.data;
1654
1701
  const interruptId = data?.interrupt_id;
1655
1702
  if (typeof interruptId !== "string" || this.#resolvedInterrupts.has(interruptId)) return;
1656
1703
  if (this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId)) return;
1704
+ const namespace = Array.isArray(event.params.namespace) ? [...event.params.namespace] : [...ROOT_NAMESPACE];
1657
1705
  const interrupt = normalizeInterruptForClient({
1658
1706
  id: interruptId,
1659
- value: data.payload
1707
+ value: data.payload,
1708
+ namespace
1660
1709
  });
1661
1710
  this.rootStore.setState((s) => {
1662
1711
  if (s.interrupts.some((entry) => entry.id === interruptId)) return s;
@@ -1669,6 +1718,21 @@ var StreamController = class {
1669
1718
  });
1670
1719
  }
1671
1720
  /**
1721
+ * Resolve the protocol namespace for a targeted resume.
1722
+ *
1723
+ * Prefer an explicit caller-supplied namespace, then the matching
1724
+ * {@link ThreadStream.interrupts} entry, then the UI mirror on
1725
+ * `rootStore.interrupts`, finally root (`[]`).
1726
+ */
1727
+ #resolveNamespaceForInterruptId(interruptId, explicitNamespace) {
1728
+ if (explicitNamespace != null) return [...explicitNamespace];
1729
+ const fromThread = this.#thread?.interrupts.find((entry) => entry.interruptId === interruptId)?.namespace;
1730
+ if (fromThread != null) return [...fromThread];
1731
+ const fromRoot = this.rootStore.getSnapshot().interrupts.find((entry) => entry.id === interruptId)?.namespace;
1732
+ if (fromRoot != null) return [...fromRoot];
1733
+ return [...ROOT_NAMESPACE];
1734
+ }
1735
+ /**
1672
1736
  * Mark an interrupt resolved for replay filtering and mirror the
1673
1737
  * removal into the root snapshot the framework hooks read.
1674
1738
  */