@ggui-ai/mcp-server 0.8.0 → 0.10.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 (67) hide show
  1. package/dist/api-renders-routes.d.ts +21 -0
  2. package/dist/api-renders-routes.d.ts.map +1 -1
  3. package/dist/api-renders-routes.js +54 -28
  4. package/dist/api-renders-stream-route.d.ts +80 -0
  5. package/dist/api-renders-stream-route.d.ts.map +1 -0
  6. package/dist/api-renders-stream-route.js +311 -0
  7. package/dist/build-mcp.d.ts +48 -7
  8. package/dist/build-mcp.d.ts.map +1 -1
  9. package/dist/build-mcp.js +87 -6
  10. package/dist/code-module-variant.d.ts +150 -0
  11. package/dist/code-module-variant.d.ts.map +1 -0
  12. package/dist/code-module-variant.js +243 -0
  13. package/dist/code-routes.d.ts +12 -2
  14. package/dist/code-routes.d.ts.map +1 -1
  15. package/dist/code-routes.js +12 -2
  16. package/dist/console-session-routes.d.ts.map +1 -1
  17. package/dist/console-session-routes.js +11 -0
  18. package/dist/control-service.d.ts +29 -3
  19. package/dist/control-service.d.ts.map +1 -1
  20. package/dist/control-service.js +26 -2
  21. package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
  22. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  23. package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
  24. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/internal-types.d.ts +63 -9
  26. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  27. package/dist/ggui-session-channel/outbound.d.ts +15 -5
  28. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  29. package/dist/ggui-session-channel/outbound.js +64 -24
  30. package/dist/ggui-session-channel/socket-router.d.ts +8 -3
  31. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/socket-router.js +6 -1
  33. package/dist/ggui-session-channel/subscribe.d.ts +48 -3
  34. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  35. package/dist/ggui-session-channel/subscribe.js +97 -36
  36. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
  37. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  38. package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
  39. package/dist/ggui-session-channel.d.ts +58 -11
  40. package/dist/ggui-session-channel.d.ts.map +1 -1
  41. package/dist/ggui-session-channel.js +55 -19
  42. package/dist/health-routes.d.ts +19 -3
  43. package/dist/health-routes.d.ts.map +1 -1
  44. package/dist/health-routes.js +26 -18
  45. package/dist/index.d.ts +6 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +13 -1
  48. package/dist/instructions-presets.js +10 -10
  49. package/dist/mcp-apps-outbound.d.ts +88 -11
  50. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  51. package/dist/mcp-apps-outbound.js +470 -77
  52. package/dist/mcp-endpoint-routes.d.ts +23 -5
  53. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  54. package/dist/mcp-endpoint-routes.js +69 -1
  55. package/dist/oauth-as-routes.d.ts +11 -0
  56. package/dist/oauth-as-routes.d.ts.map +1 -1
  57. package/dist/oauth-as-routes.js +45 -1
  58. package/dist/oauth.d.ts.map +1 -1
  59. package/dist/oauth.js +8 -1
  60. package/dist/runtime-bundle-hash.d.ts +55 -0
  61. package/dist/runtime-bundle-hash.d.ts.map +1 -0
  62. package/dist/runtime-bundle-hash.js +85 -0
  63. package/dist/runtime-bundle-route.js +1 -1
  64. package/dist/server.d.ts +239 -61
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +355 -143
  67. package/package.json +13 -12
@@ -353,7 +353,12 @@ export function attachSocketRouter(wss, deps) {
353
353
  }));
354
354
  });
355
355
  ws.on("close", () => {
356
- deps.unregister(ws);
356
+ // Resolve the subscriber bound to this socket (if any) before
357
+ // tearing it down — `unregister` is subscriber-keyed now that
358
+ // the lifecycle module is transport-neutral.
359
+ const closedSub = deps.subscribersByWs.get(ws);
360
+ if (closedSub)
361
+ deps.unregister(closedSub);
357
362
  pendingIdentity.delete(ws);
358
363
  // A pending socket that closes before subscribing frees its idle
359
364
  // timer + ceiling slot. For a socket that already subscribed,
@@ -6,12 +6,13 @@
6
6
  * reconnect-credential mint, identity-default appId resolution,
7
7
  * dev-mode render provisioning, replay, registration, and the ack.
8
8
  */
9
- import type { AuthAdapter, AuthResult, GguiSessionStore, GguiSessionStreamBuffer, StreamFanout } from "@ggui-ai/mcp-server-core";
9
+ import type { AuthAdapter, AuthResult, GguiSessionStore, GguiSessionStreamBuffer, StoredGguiSession, StreamFanout } from "@ggui-ai/mcp-server-core";
10
10
  import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
11
11
  import type { IncomingMessage } from "node:http";
12
12
  import type { WebSocket } from "ws";
13
13
  import type { Logger } from "../logger.js";
14
- import type { Outbound } from "./outbound.js";
14
+ import type { SubscriberSink } from "./internal-types.js";
15
+ import { type Outbound } from "./outbound.js";
15
16
  import type { SubscriberLifecycle } from "./subscriber-lifecycle.js";
16
17
  /**
17
18
  * Bootstrap-auth plumbing for the live-channel endpoint.
@@ -141,10 +142,39 @@ export interface SubscribeDeps {
141
142
  readonly appIdFromIdentity?: (result: AuthResult) => string;
142
143
  /** Version-handshake policy — see `GguiSessionChannelOptions.versionPolicy`. */
143
144
  readonly versionPolicy?: "advisory" | "reject";
144
- readonly send: Outbound["send"];
145
145
  readonly sendError: Outbound["sendError"];
146
146
  readonly register: SubscriberLifecycle["register"];
147
+ readonly unregister: SubscriberLifecycle["unregister"];
147
148
  }
149
+ /**
150
+ * Arguments for {@link SubscribeHandlers.completeSubscribe} — the
151
+ * transport-neutral subscribe tail. The auth head (WS: version
152
+ * handshake + bootstrap verify; SSE: the HTTP wsToken pre-gate on
153
+ * `/api/sessions/:sessionId/stream`) runs BEFORE this; the tail trusts
154
+ * `stored` + `identity` as already-authorized.
155
+ */
156
+ export type CompleteSubscribeArgs = {
157
+ /** Resolved render row the subscriber binds to. */
158
+ readonly stored: StoredGguiSession;
159
+ /** Already-verified identity for the subscriber row. */
160
+ readonly identity: AuthResult;
161
+ /** Transport write surface the tail (and the pump) emits through. */
162
+ readonly sink: SubscriberSink;
163
+ /** GguiSessionEvent-ledger cursor — replays `render_event` frames when set. */
164
+ readonly sinceSequence?: number;
165
+ /** Stream-buffer cursor — replays `data` frames per policy when set. */
166
+ readonly fromSeq?: number;
167
+ /** Correlates the ack / error frames to a WS request. SSE has none. */
168
+ readonly requestId?: string;
169
+ /** Bootstrap-minted reconnect credential to stamp on the ack. */
170
+ readonly sessionToken?: string;
171
+ } & ({
172
+ readonly transport: "ws";
173
+ /** The socket — kept on the subscriber row for inbound dispatch. */
174
+ readonly ws: WebSocket;
175
+ } | {
176
+ readonly transport: "sse";
177
+ });
148
178
  export interface SubscribeHandlers {
149
179
  /**
150
180
  * Resolve the connecting client's identity at HTTP-upgrade time,
@@ -160,6 +190,21 @@ export interface SubscribeHandlers {
160
190
  readonly sessionId: string;
161
191
  readonly appId: string;
162
192
  }): Promise<void>;
193
+ /**
194
+ * The transport-neutral subscribe tail, order preserved exactly:
195
+ * stream-cursor snapshot → stream-buffer replay read → StreamFanout
196
+ * iterator → register (pump starts) → ack → GguiSessionEvent-ledger
197
+ * replay (`render_event` frames, `resumeId` = ledger seq) →
198
+ * stream-replay `data` frames. WS `handleSubscribe` calls this after
199
+ * its auth/provision head; SSE attaches call it via
200
+ * `GguiSessionChannelServer.attachExternalSubscriber`.
201
+ *
202
+ * Returns a `detach` that idempotently unregisters the subscriber
203
+ * (ends the pump, unhooks the StreamFanout, clears polling loops).
204
+ */
205
+ completeSubscribe(args: CompleteSubscribeArgs): Promise<{
206
+ readonly detach: () => void;
207
+ }>;
163
208
  }
164
209
  export declare function createSubscribeHandlers(deps: SubscribeDeps): SubscribeHandlers;
165
210
  //# sourceMappingURL=subscribe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAMpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,uCAAuC,GAC/C;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,MAAM,wCAAwC,GAChD;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,uCAAuC,CAAC;IAC/D;;;;OAIG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5D;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,wCAAwC,CAAC;CAClE;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,WAAW,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5E;;;;OAIG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1E;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC;IAC/C,mEAAmE;IACnE,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC;IACnD,2FAA2F;IAC3F,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC5D,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;CACxD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC,GAAG,CACA;IACE,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB,GACD;IAAE,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAA;CAAE,CAChC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,0BAA0B,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtE,iEAAiE;IACjE,eAAe,CACb,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,EACjD,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACnE,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1F;AAkBD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,iBAAiB,CA+gB9E"}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { PROTOCOL_SCHEMA_VERSION, UPGRADE_REQUIRED } from "@ggui-ai/protocol";
10
10
  import { defaultAppIdFromIdentity, resolveIdentityFromHeaders, UnauthenticatedError, } from "../auth.js";
11
+ import { createWsSink } from "./outbound.js";
11
12
  /**
12
13
  * Marker `userId` (and `workspaceId`) `resolveIdentityFromUpgrade`
13
14
  * returns for a bootstrap-gated upgrade — see the "WS-token gate"
@@ -277,6 +278,24 @@ export function createSubscribeHandlers(deps) {
277
278
  return;
278
279
  }
279
280
  }
281
+ // Transport-neutral tail — shared verbatim with the SSE attach
282
+ // path (`attachExternalSubscriber`). The WS head above resolved
283
+ // auth + render; the tail owns snapshot → replay → register →
284
+ // ack → ledger replay → stream replay.
285
+ await completeSubscribe({
286
+ stored,
287
+ identity: effectiveIdentity,
288
+ sink: createWsSink(ws, deps.logger),
289
+ transport: "ws",
290
+ ws,
291
+ ...(payload.sinceSequence !== undefined ? { sinceSequence: payload.sinceSequence } : {}),
292
+ ...(payload.fromSeq !== undefined ? { fromSeq: payload.fromSeq } : {}),
293
+ ...(message.requestId ? { requestId: message.requestId } : {}),
294
+ ...(mintedSessionToken !== undefined ? { sessionToken: mintedSessionToken } : {}),
295
+ });
296
+ }
297
+ async function completeSubscribe(args) {
298
+ const { stored, sink } = args;
280
299
  // Snapshot the outbound-stream cursor BEFORE registering the
281
300
  // subscriber. Any concurrent producer that calls sendToGguiSession
282
301
  // between here and registration gets seq > snapshotSeq, so the
@@ -300,8 +319,8 @@ export function createSubscribeHandlers(deps) {
300
319
  const activeStreamSpec = activeItem.type !== "mcpApps" && activeItem.type !== "system"
301
320
  ? activeItem.streamSpec
302
321
  : undefined;
303
- const replay = payload.fromSeq !== undefined
304
- ? await deps.streamBuffer.replay(stored.id, payload.fromSeq, activeStreamSpec)
322
+ const replay = args.fromSeq !== undefined
323
+ ? await deps.streamBuffer.replay(stored.id, args.fromSeq, activeStreamSpec)
305
324
  : await deps.streamBuffer.replay(stored.id, 0, undefined);
306
325
  // Subscribe to the StreamFanout BEFORE constructing the Subscriber:
307
326
  // the seam returns an AsyncIterable whose iterator we hand off; the
@@ -310,29 +329,34 @@ export function createSubscribeHandlers(deps) {
310
329
  // point onward queues into our iterator — paired with the
311
330
  // replayCompletedSeq cursor below, that's race-free.
312
331
  const fanoutIter = deps.streamFanout.subscribe(stored.id)[Symbol.asyncIterator]();
313
- const sub = {
314
- ws,
332
+ const base = {
315
333
  sessionId: stored.id,
316
334
  appId: stored.appId,
317
- identity: effectiveIdentity,
335
+ identity: args.identity,
318
336
  connectedAt: Date.now(),
319
337
  replayCompletedSeq: snapshotSeq,
320
338
  iter: fanoutIter,
321
339
  // Per-subscriber channel-subscribe tracker. Populated
322
340
  // lazily by the `channel_subscribe` handler when the operator
323
- // wired `streamWebSocketLocalTools`; stays empty otherwise.
341
+ // wired `streamWebSocketLocalTools`; stays empty otherwise
342
+ // (structurally so for SSE — no inbound channel).
324
343
  channelSubs: new Map(),
344
+ sink,
325
345
  };
346
+ const sub = args.transport === "ws"
347
+ ? { ...base, transport: "ws", ws: args.ws }
348
+ : { ...base, transport: "sse" };
326
349
  deps.register(sub);
327
350
  deps.logger.info("render_channel_subscribed", {
328
351
  sessionId: stored.id,
329
352
  appId: stored.appId,
330
- identityKind: effectiveIdentity.identity.kind,
331
- fromSeq: payload.fromSeq,
353
+ identityKind: args.identity.identity.kind,
354
+ transport: args.transport,
355
+ fromSeq: args.fromSeq,
332
356
  snapshotSeq,
333
357
  replayCount: replay?.envelopes.length ?? 0,
334
358
  replayTruncated: replay?.truncated ?? false,
335
- bootstrap: mintedSessionToken !== undefined,
359
+ bootstrap: args.sessionToken !== undefined,
336
360
  });
337
361
  const ackPayload = {
338
362
  sequence: stored.eventSequence,
@@ -346,14 +370,14 @@ export function createSubscribeHandlers(deps) {
346
370
  // the handshake ignore the field (legacy-pass-through).
347
371
  serverVersion: PROTOCOL_SCHEMA_VERSION,
348
372
  ...(replay?.truncated ? { replayTruncated: true } : {}),
349
- ...(mintedSessionToken !== undefined ? { sessionToken: mintedSessionToken } : {}),
373
+ ...(args.sessionToken !== undefined ? { sessionToken: args.sessionToken } : {}),
350
374
  };
351
- deps.send(ws, {
375
+ sink.write({
352
376
  type: "ack",
353
377
  payload: ackPayload,
354
- ...(message.requestId ? { requestId: message.requestId } : {}),
378
+ ...(args.requestId ? { requestId: args.requestId } : {}),
355
379
  });
356
- // R7 — GguiSessionEvent ledger replay. When `payload.sinceSequence` is
380
+ // R7 — GguiSessionEvent ledger replay. When `args.sinceSequence` is
357
381
  // present, fetch events with `seq > sinceSequence` from the per-
358
382
  // render ledger and emit each as a `render_event` wire frame
359
383
  // BEFORE the per-channel stream-buffer replay. Consumers dispatch
@@ -361,38 +385,70 @@ export function createSubscribeHandlers(deps) {
361
385
  // (render/props_update/etc.) — same cursor model as the HTTP
362
386
  // `/api/sessions/:id/events?sinceSequence=N` endpoint.
363
387
  //
388
+ // Each replay frame carries `resumeId` = the ledger seq — the SSE
389
+ // sink stamps it as the `id:` field so the browser's Last-Event-ID
390
+ // lands on the exact same cursor space; the WS sink ignores it.
391
+ //
392
+ // Pagination: a hasMore-loop over 100-event pages (the HTTP
393
+ // route's default page size). Strict superset of the previous
394
+ // single-page-100 WS behavior.
395
+ //
364
396
  // Horizon gate: a cursor below the server's replay horizon OR
365
397
  // above `lastSequence` (stale from a different deployment) emits
366
398
  // an error frame with `code: 'REPLAY_HORIZON_PASSED'` and skips
367
- // the replay. Client recovery: re-mount from a fresh /state read.
368
- if (payload.sinceSequence !== undefined) {
369
- const sinceSeq = payload.sinceSequence;
399
+ // the replay. The frame carries `resumeId` = lastSequence — a
400
+ // dispatched id-bearing frame advances the browser's Last-Event-ID
401
+ // to the fresh high-water mark, and the ack snapshot already
402
+ // re-mounted state. Client recovery: re-mount from a fresh /state
403
+ // read (WS) / the already-delivered ack (SSE).
404
+ if (args.sinceSequence !== undefined) {
405
+ const sinceSeq = args.sinceSequence;
370
406
  if (sinceSeq < 0 || !Number.isInteger(sinceSeq)) {
371
- deps.sendError(ws, "INVALID_SINCE_SEQUENCE", "sinceSequence must be a non-negative integer", message.requestId);
407
+ sink.write({
408
+ type: "error",
409
+ payload: {
410
+ code: "INVALID_SINCE_SEQUENCE",
411
+ message: "sinceSequence must be a non-negative integer",
412
+ },
413
+ ...(args.requestId ? { requestId: args.requestId } : {}),
414
+ });
372
415
  }
373
416
  else {
374
- const ledger = await deps.renderStore.listEventsSince(stored.id, sinceSeq,
375
- // Server-side cap matches the HTTP route's default (100).
376
- // Stress + replay-from-zero workloads cap here.
377
- 100);
378
- if (ledger === null) {
379
- // GguiSession disappeared between resolve and ledger read —
380
- // already handled by the broader error envelope path; nothing
381
- // to do here.
382
- }
383
- else if (sinceSeq > ledger.lastSequence || sinceSeq < ledger.horizonSeq) {
384
- deps.sendError(ws, "REPLAY_HORIZON_PASSED", `cursor ${sinceSeq} is outside replayable range [${ledger.horizonSeq}, ${ledger.lastSequence}]`, message.requestId, { currentSequence: ledger.lastSequence });
385
- }
386
- else {
417
+ let cursor = sinceSeq;
418
+ let firstPage = true;
419
+ for (;;) {
420
+ const ledger = await deps.renderStore.listEventsSince(stored.id, cursor, 100);
421
+ if (ledger === null) {
422
+ // GguiSession disappeared between resolve and ledger read —
423
+ // already handled by the broader error envelope path;
424
+ // nothing to do here.
425
+ break;
426
+ }
427
+ if (firstPage && (sinceSeq > ledger.lastSequence || sinceSeq < ledger.horizonSeq)) {
428
+ sink.write({
429
+ type: "error",
430
+ payload: {
431
+ code: "REPLAY_HORIZON_PASSED",
432
+ message: `cursor ${sinceSeq} is outside replayable range [${ledger.horizonSeq}, ${ledger.lastSequence}]`,
433
+ details: { currentSequence: ledger.lastSequence },
434
+ },
435
+ ...(args.requestId ? { requestId: args.requestId } : {}),
436
+ }, { resumeId: String(ledger.lastSequence) });
437
+ break;
438
+ }
439
+ firstPage = false;
387
440
  for (const event of ledger.events) {
388
441
  // GguiSessionEvent is now the wire-shape ledger primitive
389
442
  // (Wave 7 of flatten-render-identity, 2026-05-28); no
390
443
  // projection — emit the store's row directly.
391
- deps.send(ws, {
392
- type: "render_event",
393
- payload: event,
394
- });
444
+ sink.write({ type: "render_event", payload: event }, { resumeId: String(event.seq) });
445
+ cursor = event.seq;
395
446
  }
447
+ // Defensive: an empty page with hasMore would loop forever;
448
+ // the store contract never produces it, but a broken adapter
449
+ // must not spin the server.
450
+ if (!ledger.hasMore || ledger.events.length === 0)
451
+ break;
396
452
  }
397
453
  }
398
454
  }
@@ -403,9 +459,14 @@ export function createSubscribeHandlers(deps) {
403
459
  // the single source of truth for ordering.
404
460
  if (replay) {
405
461
  for (const env of replay.envelopes) {
406
- deps.send(ws, { type: "data", payload: env });
462
+ sink.write({ type: "data", payload: env });
407
463
  }
408
464
  }
465
+ return {
466
+ detach: () => {
467
+ deps.unregister(sub);
468
+ },
469
+ };
409
470
  }
410
- return { resolveIdentityFromUpgrade, handleSubscribe };
471
+ return { resolveIdentityFromUpgrade, handleSubscribe, completeSubscribe };
411
472
  }
@@ -1,26 +1,35 @@
1
1
  /**
2
- * WS subscriber lifecycle for the live channel — registration into the
2
+ * Subscriber lifecycle for the live channel — registration into the
3
3
  * shared subscriber set, the per-subscriber live-tail pump loop, and
4
4
  * the symmetric teardown path (`unregister`) that ends the pump,
5
5
  * unhooks the StreamFanout subscription, and clears every
6
6
  * `channel_subscribe` polling loop the subscriber owned.
7
7
  *
8
+ * Transport-neutral: subscribers write through their `SubscriberSink`,
9
+ * so WS-attached and SSE-attached subscribers share this module
10
+ * unchanged. Only the ws→subscriber reverse index is WS-specific —
11
+ * it is populated for `transport: 'ws'` rows only, because it exists
12
+ * for the inbound socket-router dispatch SSE does not have.
13
+ *
8
14
  * Owns the per-render subscriber counter that drives the
9
15
  * `onFirstSubscriber` / `onLastSubscriberGone` 0↔1 transition hooks —
10
- * no other module reads it.
16
+ * no other module reads it. The hooks fire for SSE attaches too:
17
+ * hosted cross-pod pubsub scoping keys on "does this pod hold any
18
+ * subscriber for this render", regardless of transport.
11
19
  */
12
- import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
13
20
  import type { WebSocket } from "ws";
14
21
  import type { Logger } from "../logger.js";
15
- import type { Subscriber } from "./internal-types.js";
22
+ import type { Subscriber, WsSubscriber } from "./internal-types.js";
16
23
  export interface SubscriberLifecycleDeps {
17
24
  readonly logger: Logger;
18
- /** Flat set of all live WS subscribers — membership owned HERE. */
25
+ /** Flat set of all live subscribers (WS + SSE) — membership owned HERE. */
19
26
  readonly wsSubscribers: Set<Subscriber>;
20
- /** ws → subscriber reverse index so socket-close can look up cheaply. */
21
- readonly subscribersByWs: WeakMap<WebSocket, Subscriber>;
22
- /** Low-level wire write from the outbound module. */
23
- readonly send: (ws: WebSocket, msg: WebSocketMessage) => void;
27
+ /**
28
+ * ws → subscriber reverse index so socket-close / inbound dispatch
29
+ * can look up cheaply. WS-only by construction: populated for
30
+ * `transport: 'ws'` subscribers only.
31
+ */
32
+ readonly subscribersByWs: WeakMap<WebSocket, WsSubscriber>;
24
33
  /** 0→1 transition hook — see `GguiSessionChannelOptions.onFirstSubscriber`. */
25
34
  readonly onFirstSubscriber?: (sessionId: string) => void;
26
35
  /** 1→0 transition hook — see `GguiSessionChannelOptions.onLastSubscriberGone`. */
@@ -30,11 +39,12 @@ export interface SubscriberLifecycle {
30
39
  /** Add a subscriber to the live set and start its pump loop. */
31
40
  register(sub: Subscriber): void;
32
41
  /**
33
- * Tear down the subscriber bound to `ws` (if any): remove from the
34
- * live set, end the fanout iterator (terminates the pump), clear
35
- * every channel-subscribe polling timer. Idempotent.
42
+ * Tear down `sub` (if still registered): remove from the live set,
43
+ * end the fanout iterator (terminates the pump), clear every
44
+ * channel-subscribe polling timer. Idempotent — a second call for
45
+ * the same subscriber is a no-op.
36
46
  */
37
- unregister(ws: WebSocket): void;
47
+ unregister(sub: Subscriber): void;
38
48
  }
39
49
  export declare function createSubscriberLifecycle(deps: SubscriberLifecycleDeps): SubscriberLifecycle;
40
50
  //# sourceMappingURL=subscriber-lifecycle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber-lifecycle.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscriber-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACxC,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzD,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC9D,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kFAAkF;IAClF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;CACjC;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB,CA4G5F"}
1
+ {"version":3,"file":"subscriber-lifecycle.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscriber-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kFAAkF;IAClF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC;;;;;OAKG;IACH,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB,CAiH5F"}
@@ -1,13 +1,21 @@
1
1
  /**
2
- * WS subscriber lifecycle for the live channel — registration into the
2
+ * Subscriber lifecycle for the live channel — registration into the
3
3
  * shared subscriber set, the per-subscriber live-tail pump loop, and
4
4
  * the symmetric teardown path (`unregister`) that ends the pump,
5
5
  * unhooks the StreamFanout subscription, and clears every
6
6
  * `channel_subscribe` polling loop the subscriber owned.
7
7
  *
8
+ * Transport-neutral: subscribers write through their `SubscriberSink`,
9
+ * so WS-attached and SSE-attached subscribers share this module
10
+ * unchanged. Only the ws→subscriber reverse index is WS-specific —
11
+ * it is populated for `transport: 'ws'` rows only, because it exists
12
+ * for the inbound socket-router dispatch SSE does not have.
13
+ *
8
14
  * Owns the per-render subscriber counter that drives the
9
15
  * `onFirstSubscriber` / `onLastSubscriberGone` 0↔1 transition hooks —
10
- * no other module reads it.
16
+ * no other module reads it. The hooks fire for SSE attaches too:
17
+ * hosted cross-pod pubsub scoping keys on "does this pod hold any
18
+ * subscriber for this render", regardless of transport.
11
19
  */
12
20
  export function createSubscriberLifecycle(deps) {
13
21
  /**
@@ -21,7 +29,7 @@ export function createSubscriberLifecycle(deps) {
21
29
  const renderCountById = new Map();
22
30
  /**
23
31
  * Pump live frames from the StreamFanout iterator out to this
24
- * subscriber's WS. Started fire-and-forget by `register`; ends when
32
+ * subscriber's sink. Started fire-and-forget by `register`; ends when
25
33
  * the iterator yields done (close() on the seam) OR `unregister`
26
34
  * calls `iter.return()`. Per-subscriber seq filter applied here:
27
35
  * frames with `seq <= replayCompletedSeq` were (or will be)
@@ -42,11 +50,11 @@ export function createSubscriberLifecycle(deps) {
42
50
  return;
43
51
  if (value.seq <= sub.replayCompletedSeq)
44
52
  continue;
45
- if (sub.ws.readyState !== sub.ws.OPEN) {
53
+ if (!sub.sink.isOpen()) {
46
54
  await sub.iter.return?.();
47
55
  return;
48
56
  }
49
- deps.send(sub.ws, { type: "data", payload: value });
57
+ sub.sink.write({ type: "data", payload: value });
50
58
  }
51
59
  }
52
60
  catch (err) {
@@ -58,7 +66,9 @@ export function createSubscriberLifecycle(deps) {
58
66
  }
59
67
  function register(sub) {
60
68
  deps.wsSubscribers.add(sub);
61
- deps.subscribersByWs.set(sub.ws, sub);
69
+ if (sub.transport === "ws") {
70
+ deps.subscribersByWs.set(sub.ws, sub);
71
+ }
62
72
  // Per-render count bookkeeping + 0→1 hook for cloud pubsub
63
73
  // adapter scoping. Increment FIRST so the hook sees the up-to-date
64
74
  // state; hook fires only on the transition (prevCount === 0).
@@ -70,7 +80,7 @@ export function createSubscriberLifecycle(deps) {
70
80
  }
71
81
  catch (err) {
72
82
  // Best-effort: a thrown hook MUST NOT corrupt the
73
- // wsSubscribers set vs the real socket lifecycle.
83
+ // wsSubscribers set vs the real transport lifecycle.
74
84
  deps.logger.warn("render_channel_on_first_subscriber_threw", {
75
85
  sessionId: sub.sessionId,
76
86
  error: String(err),
@@ -81,12 +91,15 @@ export function createSubscriberLifecycle(deps) {
81
91
  // inside pumpSubscriber, never propagated.
82
92
  void pumpSubscriber(sub);
83
93
  }
84
- function unregister(ws) {
85
- const sub = deps.subscribersByWs.get(ws);
86
- if (!sub)
94
+ function unregister(sub) {
95
+ // Idempotency gate: membership in the live set is the single
96
+ // "still registered" signal, shared across transports.
97
+ if (!deps.wsSubscribers.has(sub))
87
98
  return;
88
- deps.subscribersByWs.delete(ws);
89
99
  deps.wsSubscribers.delete(sub);
100
+ if (sub.transport === "ws") {
101
+ deps.subscribersByWs.delete(sub.ws);
102
+ }
90
103
  // Per-render count bookkeeping + 1→0 hook (symmetric with register).
91
104
  const prevCount = renderCountById.get(sub.sessionId) ?? 0;
92
105
  if (prevCount <= 1) {
@@ -20,11 +20,13 @@
20
20
  * mutation handlers (ggui_emit / connector `ctx.send`). Validated
21
21
  * through `assertStreamContract` before delivery.
22
22
  *
23
- * `props_update`: the agent-driven `ggui_update` handler calls
24
- * `channel.sendPropsUpdate(sessionId, props)` (wired as its
25
- * `propsUpdateNotifier`) to fan a `{type:'props_update'}` frame to
26
- * live subscribers. Reaches the renderer's existing `props_update`
27
- * branch in `iframe-runtime` and applies new props in-place.
23
+ * `props_update`: the agent-driven `ggui_update` / `ggui_amend`
24
+ * handlers call `channel.sendPropsUpdate(sessionId, props, epoch)`
25
+ * (wired as their `propsUpdateNotifier`) to fan a
26
+ * `{type:'props_update'}` frame to live subscribers. Reaches the
27
+ * renderer's `props_update` branch in `iframe-runtime`, which applies
28
+ * new props in-place — or freezes the mount when the frame's `epoch`
29
+ * exceeds its own (#483 freeze latch).
28
30
  *
29
31
  * Not handled here:
30
32
  *
@@ -56,6 +58,7 @@ import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
56
58
  import type { IncomingMessage } from "node:http";
57
59
  import type { Duplex } from "node:stream";
58
60
  import { type GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
61
+ import type { SubscriberSink } from "./ggui-session-channel/internal-types.js";
59
62
  import { type GguiSessionChannelBootstrap, type GguiSessionChannelCookieAuth } from "./ggui-session-channel/subscribe.js";
60
63
  import type { Logger } from "./logger.js";
61
64
  /** Default URL path for the channel endpoint. Operators can override. */
@@ -110,6 +113,17 @@ export declare const DEFAULT_PRE_SUBSCRIBE_IDLE_MS = 30000;
110
113
  export declare const DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS = 1024;
111
114
  export type { GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
112
115
  export type { GguiSessionChannelBootstrap, GguiSessionChannelBootstrapRefreshResult, GguiSessionChannelBootstrapVerifyResult, GguiSessionChannelCookieAuth, } from "./ggui-session-channel/subscribe.js";
116
+ export type { SubscriberSink } from "./ggui-session-channel/internal-types.js";
117
+ /**
118
+ * Thrown by {@link GguiSessionChannelServer.attachExternalSubscriber}
119
+ * when the target render does not exist OR is bound to a different
120
+ * app than the caller claims. Callers that pre-gated (the SSE route
121
+ * does) treat this as a lost race with eviction; callers that did not
122
+ * MUST map it to their 404 surface.
123
+ */
124
+ export declare class ChannelSessionNotFoundError extends Error {
125
+ constructor(sessionId: string);
126
+ }
113
127
  export interface GguiSessionChannelOptions {
114
128
  /** Required — the render backing store (typically `InMemoryGguiSessionStore`). */
115
129
  readonly renderStore: GguiSessionStore;
@@ -432,11 +446,13 @@ export interface GguiSessionChannelServer {
432
446
  */
433
447
  notifyGguiSessionCommit(sessionId: string, render: GguiSession, matchType?: string): void;
434
448
  /**
435
- * Fan a `{type:'props_update', payload:{sessionId, props}}` wire frame
436
- * to every subscriber currently bound to `sessionId`. The agent-driven
437
- * `ggui_update` handler calls this (wired as its `propsUpdateNotifier`)
438
- * so a props patch replaces renderer props in-place on live
439
- * subscribers without waiting for a resubscribe.
449
+ * Fan a `{type:'props_update', payload:{sessionId, props, epoch}}`
450
+ * wire frame to every subscriber currently bound to `sessionId`.
451
+ * Both mutation handlers call this (wired as their
452
+ * `propsUpdateNotifier`): `ggui_amend` fans the unchanged head
453
+ * epoch so the live mount repaints in place, `ggui_update` fans its
454
+ * freshly-advanced epoch so superseded mounts freeze (#483 latch)
455
+ * — no waiting for a resubscribe either way.
440
456
  *
441
457
  * Validation posture (mirrors `notifyGguiSessionCommit`'s "best-effort orphan
442
458
  * no-op"):
@@ -464,7 +480,7 @@ export interface GguiSessionChannelServer {
464
480
  * `render.propsSpec` before applying — defense-in-depth at the
465
481
  * receiving boundary.
466
482
  */
467
- sendPropsUpdate(sessionId: string, props: JsonObject): Promise<void>;
483
+ sendPropsUpdate(sessionId: string, props: JsonObject, epoch: number): Promise<void>;
468
484
  /**
469
485
  * Fan a `{type:'drain_ack', payload:{sessionId, appId,
470
486
  * eventId, drainedAt}}` wire frame to every subscriber currently
@@ -508,6 +524,37 @@ export interface GguiSessionChannelServer {
508
524
  * subscriber send failures are logged but never propagated.
509
525
  */
510
526
  externalBroadcast(sessionId: string, frame: WebSocketMessage): void;
527
+ /**
528
+ * Register an externally-transported (non-WS) subscriber into the
529
+ * SAME fan-out planes WS subscribers ride: the StreamFanout live
530
+ * tail, the direct walks (`props_update` / `render` / `drain_ack` /
531
+ * external broadcasts), and the subscribe tail's ack → ledger-replay
532
+ * → stream-replay ordering. The single consumer today is the SSE
533
+ * route (`GET /api/sessions/:sessionId/stream`).
534
+ *
535
+ * The caller owns transport-level auth (the SSE route clones the
536
+ * /state wsToken gate) — this method only re-checks render existence
537
+ * + appId binding as defense-in-depth, throwing
538
+ * {@link ChannelSessionNotFoundError} on mismatch.
539
+ *
540
+ * `sinceSequence` replays the GguiSessionEvent ledger as
541
+ * `render_event` frames (each written with `resumeId` = ledger seq —
542
+ * the SSE sink stamps it as the `id:` field); `fromSeq` replays the
543
+ * stream buffer as `data` frames. Returns `detach` — idempotent
544
+ * teardown that unregisters the subscriber, ends its pump, and fires
545
+ * the 1→0 subscriber hook when applicable. The caller MUST invoke it
546
+ * on transport close.
547
+ */
548
+ attachExternalSubscriber(args: {
549
+ readonly sessionId: string;
550
+ readonly appId: string;
551
+ readonly identity: AuthResult;
552
+ readonly sink: SubscriberSink;
553
+ readonly sinceSequence?: number;
554
+ readonly fromSeq?: number;
555
+ }): Promise<{
556
+ readonly detach: () => void;
557
+ }>;
511
558
  /** Number of live subscribers. Useful for health / debug introspection. */
512
559
  readonly subscriberCount: number;
513
560
  /** Number of distinct renders with at least one subscriber. */
@@ -1 +1 @@
1
- {"version":3,"file":"ggui-session-channel.d.ts","sourceRoot":"","sources":["../src/ggui-session-channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ggui-session-channel.d.ts","sourceRoot":"","sources":["../src/ggui-session-channel.ts"],"names":[],"mappings":"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"}