@ggui-ai/mcp-server 0.25.0 → 0.26.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/api-renders-routes.d.ts +46 -5
  3. package/dist/api-renders-routes.d.ts.map +1 -1
  4. package/dist/api-renders-routes.js +98 -39
  5. package/dist/api-renders-stream-route.d.ts +2 -1
  6. package/dist/api-renders-stream-route.d.ts.map +1 -1
  7. package/dist/api-renders-stream-route.js +14 -2
  8. package/dist/build-mcp.d.ts +16 -0
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +48 -20
  11. package/dist/console-auth.d.ts +10 -10
  12. package/dist/console-auth.d.ts.map +1 -1
  13. package/dist/first-seen.d.ts +14 -0
  14. package/dist/first-seen.d.ts.map +1 -0
  15. package/dist/first-seen.js +19 -0
  16. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  17. package/dist/ggui-session-channel/action-ingress.js +96 -8
  18. package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
  19. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  20. package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
  21. package/dist/ggui-session-channel/internal-types.d.ts +20 -12
  22. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  23. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  24. package/dist/ggui-session-channel/outbound.js +18 -6
  25. package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
  26. package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
  27. package/dist/ggui-session-channel/stream-epoch.js +47 -0
  28. package/dist/ggui-session-channel/subscribe.d.ts +29 -17
  29. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  30. package/dist/ggui-session-channel/subscribe.js +247 -124
  31. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
  33. package/dist/ggui-session-channel.d.ts +13 -5
  34. package/dist/ggui-session-channel.d.ts.map +1 -1
  35. package/dist/ggui-session-channel.js +2 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/instructions-presets.d.ts +22 -21
  40. package/dist/instructions-presets.d.ts.map +1 -1
  41. package/dist/instructions-presets.js +38 -38
  42. package/dist/llm-backed-negotiator.d.ts +1 -36
  43. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  44. package/dist/llm-backed-negotiator.js +0 -196
  45. package/dist/mcp-apps-outbound.d.ts +39 -90
  46. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  47. package/dist/mcp-apps-outbound.js +45 -3
  48. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  49. package/dist/mcp-endpoint-routes.js +4 -1
  50. package/dist/server.d.ts +108 -33
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +181 -48
  53. package/dist/storage.d.ts +9 -1
  54. package/dist/storage.d.ts.map +1 -1
  55. package/dist/storage.js +4 -3
  56. package/dist/view-proof-gate.d.ts +96 -0
  57. package/dist/view-proof-gate.d.ts.map +1 -0
  58. package/dist/view-proof-gate.js +167 -0
  59. package/package.json +14 -15
@@ -2,8 +2,8 @@
2
2
  * Subscribe / credential-path family for the live channel — upgrade-
3
3
  * time identity resolution across the three auth planes (bearer via
4
4
  * the AuthAdapter, ws-token bootstrap, console cookie) and the
5
- * `subscribe` handler itself: version handshake, bootstrap verify +
6
- * reconnect-credential mint, identity-default appId resolution,
5
+ * `subscribe` handler itself: version handshake, bootstrap verify,
6
+ * identity-default appId resolution,
7
7
  * dev-mode render provisioning, replay, registration, and the ack.
8
8
  */
9
9
  import { PROTOCOL_SCHEMA_VERSION, UPGRADE_REQUIRED } from "@ggui-ai/protocol";
@@ -23,6 +23,56 @@ const BOOTSTRAP_PENDING_MARKER = "__bootstrap_pending__";
23
23
  function isBootstrapPendingIdentity(identity) {
24
24
  return identity.identity.kind === "user" && identity.identity.userId === BOOTSTRAP_PENDING_MARKER;
25
25
  }
26
+ /**
27
+ * A subscriber's sink as every writer other than the subscribe tail sees it
28
+ * (ggui#1525). Until {@link release}, frames are held in arrival order;
29
+ * `release` flushes them and passes every later write straight through.
30
+ *
31
+ * The tail writes the ack and the replay frames through the raw sink, and
32
+ * awaits the ledger read in between when `sinceSequence` is set. Meanwhile
33
+ * the subscriber is registered, so the live pump, the direct walks (a
34
+ * `render` or `props_update`, a `drain_ack`, a cross-replica
35
+ * `externalBroadcast`) and a source-fed `channel_payload` poll can already
36
+ * write to it. Held here, their frames reach the wire after the replay,
37
+ * which is the order the ack promises: ack → replay → live.
38
+ *
39
+ * One writer stays outside, by design: `channel_error` goes straight to
40
+ * the socket, both when it answers the client's own `channel_subscribe`
41
+ * and when a source poll reports `POLL_FAILED`. It carries no stream
42
+ * state, and neither can precede the release: a poll exists only after a
43
+ * processed `channel_subscribe`, which the per-socket inbound chain
44
+ * handles after the subscribe resolves.
45
+ *
46
+ * A read that stalls holds these frames for as long as it stalls; the
47
+ * store's own read timeout is what bounds it.
48
+ */
49
+ function holdUntilReplayed(sink) {
50
+ let held = [];
51
+ return {
52
+ sink: {
53
+ isOpen: () => sink.isOpen(),
54
+ end: (reason) => sink.end(reason),
55
+ write: (frame, opts) => {
56
+ if (held !== undefined) {
57
+ held.push({ frame, opts });
58
+ return;
59
+ }
60
+ sink.write(frame, opts);
61
+ },
62
+ },
63
+ release: () => {
64
+ const queue = held;
65
+ held = undefined;
66
+ if (queue === undefined)
67
+ return;
68
+ for (const { frame, opts } of queue) {
69
+ if (!sink.isOpen())
70
+ return;
71
+ sink.write(frame, opts);
72
+ }
73
+ },
74
+ };
75
+ }
26
76
  export function createSubscribeHandlers(deps) {
27
77
  async function resolveIdentityFromUpgrade(req) {
28
78
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -166,7 +216,7 @@ export function createSubscribeHandlers(deps) {
166
216
  // AuthAdapter identity — iframes don't carry bearer tokens.
167
217
  // Mutually-exclusive on purpose.
168
218
  let effectiveIdentity = identity;
169
- let mintedSessionToken;
219
+ let viaWsToken = false;
170
220
  let tokenBoundAppId;
171
221
  if (typeof payload.wsToken === "string" && payload.wsToken.length > 0) {
172
222
  if (!deps.bootstrap) {
@@ -221,10 +271,7 @@ export function createSubscribeHandlers(deps) {
221
271
  },
222
272
  source: "apikey",
223
273
  };
224
- // Mint the reconnect credential now — before create/observe
225
- // work — so a downstream failure doesn't leave the client with
226
- // no way to resume.
227
- mintedSessionToken = deps.bootstrap.issueSessionToken(bound.sessionId, bound.appId);
274
+ viaWsToken = true;
228
275
  deps.logger.info("render_channel_bootstrap_accepted", {
229
276
  sessionId: bound.sessionId,
230
277
  appId: bound.appId,
@@ -331,8 +378,13 @@ export function createSubscribeHandlers(deps) {
331
378
  ws,
332
379
  ...(payload.sinceSequence !== undefined ? { sinceSequence: payload.sinceSequence } : {}),
333
380
  ...(payload.fromSeq !== undefined ? { fromSeq: payload.fromSeq } : {}),
381
+ ...(typeof payload.fromEpoch === "string" && payload.fromEpoch !== "" ? { fromEpoch: payload.fromEpoch } : {}),
334
382
  ...(message.requestId ? { requestId: message.requestId } : {}),
335
- ...(mintedSessionToken !== undefined ? { sessionToken: mintedSessionToken } : {}),
383
+ ...(viaWsToken
384
+ ? { credential: "ws_token" }
385
+ : cookieBound !== undefined
386
+ ? { credential: "console_cookie" }
387
+ : {}),
336
388
  });
337
389
  }
338
390
  async function completeSubscribe(args) {
@@ -342,10 +394,18 @@ export function createSubscribeHandlers(deps) {
342
394
  // between here and registration gets seq > snapshotSeq, so the
343
395
  // subscriber will receive it via live fan-out (not via replay).
344
396
  //
345
- // This is race-safe in single-threaded JS: the next few lines run
346
- // synchronously up to `register(sub)`, and fan-out's per-subscriber
347
- // `seq <= replayCompletedSeq` guard takes care of the window.
348
- const snapshotSeq = await deps.streamBuffer.currentSeq(stored.id);
397
+ // This is race-safe: a delivery landing between this read and
398
+ // `register(sub)` (the replay read below is awaited) is either in the
399
+ // replay or published to the fan-out iterator subscribed below, and
400
+ // the per-subscriber `seq <= replayCompletedSeq` guard drops the
401
+ // copies the replay already carries.
402
+ //
403
+ // The seq and the epoch it counts in are read at once when the buffer
404
+ // can (ggui#1531); a buffer that predates epochs gives the seq alone.
405
+ const snapshot = deps.streamBuffer.currentCursor !== undefined
406
+ ? await deps.streamBuffer.currentCursor(stored.id)
407
+ : { seq: await deps.streamBuffer.currentSeq(stored.id) };
408
+ const snapshotSeq = snapshot.seq;
349
409
  // Phase B: a render IS the addressable unit, so the active item
350
410
  // is the resolved render's visible-bits surface itself.
351
411
  const activeItem = stored.render;
@@ -360,8 +420,16 @@ export function createSubscribeHandlers(deps) {
360
420
  const activeStreamSpec = activeItem.type !== "mcpApps" && activeItem.type !== "system"
361
421
  ? activeItem.streamSpec
362
422
  : undefined;
423
+ //
424
+ // A resume whose cursor belongs to a counter that has since restarted
425
+ // (ggui#1531) replays everything retained, from 0, and reports the gap:
426
+ // its epoch differs from the session's, or, from a client that sent no
427
+ // epoch, its `fromSeq` is above anything the counter has assigned.
428
+ const counterRestarted = args.fromSeq !== undefined &&
429
+ ((args.fromEpoch !== undefined && snapshot.epoch !== undefined && args.fromEpoch !== snapshot.epoch) ||
430
+ args.fromSeq > snapshotSeq);
363
431
  const replay = args.fromSeq !== undefined
364
- ? await deps.streamBuffer.replay(stored.id, args.fromSeq, activeStreamSpec)
432
+ ? await deps.streamBuffer.replay(stored.id, counterRestarted ? 0 : args.fromSeq, activeStreamSpec)
365
433
  : await deps.streamBuffer.replay(stored.id, 0, undefined);
366
434
  // Subscribe to the StreamFanout BEFORE constructing the Subscriber:
367
435
  // the seam returns an AsyncIterable whose iterator we hand off; the
@@ -370,139 +438,194 @@ export function createSubscribeHandlers(deps) {
370
438
  // point onward queues into our iterator — paired with the
371
439
  // replayCompletedSeq cursor below, that's race-free.
372
440
  const fanoutIter = deps.streamFanout.subscribe(stored.id)[Symbol.asyncIterator]();
441
+ const gate = holdUntilReplayed(sink);
373
442
  const base = {
374
443
  sessionId: stored.id,
375
444
  appId: stored.appId,
376
445
  identity: args.identity,
377
446
  connectedAt: Date.now(),
378
- replayCompletedSeq: snapshotSeq,
447
+ replayCursor: { seq: snapshotSeq, epoch: snapshot.epoch },
379
448
  iter: fanoutIter,
380
449
  // Per-subscriber channel-subscribe tracker. Populated
381
450
  // lazily by the `channel_subscribe` handler when the operator
382
451
  // wired `streamWebSocketLocalTools`; stays empty otherwise
383
452
  // (structurally so for SSE — no inbound channel).
384
453
  channelSubs: new Map(),
385
- sink,
454
+ // Every writer but this tail goes through the gate until the
455
+ // replay is on the wire (ggui#1525); the tail writes to `sink`.
456
+ sink: gate.sink,
386
457
  };
387
458
  const sub = args.transport === "ws"
388
459
  ? { ...base, transport: "ws", ws: args.ws }
389
460
  : { ...base, transport: "sse" };
390
461
  deps.register(sub);
391
- deps.logger.info("render_channel_subscribed", {
392
- sessionId: stored.id,
393
- appId: stored.appId,
394
- identityKind: args.identity.identity.kind,
395
- transport: args.transport,
396
- fromSeq: args.fromSeq,
397
- snapshotSeq,
398
- replayCount: replay?.envelopes.length ?? 0,
399
- replayTruncated: replay?.truncated ?? false,
400
- bootstrap: args.sessionToken !== undefined,
401
- });
402
- const ackPayload = {
403
- sequence: stored.eventSequence,
404
- timestamp: Date.now(),
405
- session: stored.render,
406
- streamSeq: snapshotSeq,
407
- // Advertise the server's protocol version on every successful
408
- // subscribe ack (SPEC §11.2.2). Clients whose
409
- // CLIENT_SUPPORTED_VERSIONS doesn't contain this string surface
410
- // UpgradeRequiredError to their caller; clients that don't wire
411
- // the handshake ignore the field (legacy-pass-through).
412
- serverVersion: PROTOCOL_SCHEMA_VERSION,
413
- ...(replay?.truncated ? { replayTruncated: true } : {}),
414
- ...(args.sessionToken !== undefined ? { sessionToken: args.sessionToken } : {}),
415
- };
416
- sink.write({
417
- type: "ack",
418
- payload: ackPayload,
419
- ...(args.requestId ? { requestId: args.requestId } : {}),
420
- });
421
- // R7 — GguiSessionEvent ledger replay. When `args.sinceSequence` is
422
- // present, fetch events with `seq > sinceSequence` from the per-
423
- // render ledger and emit each as a `render_event` wire frame
424
- // BEFORE the per-channel stream-buffer replay. Consumers dispatch
425
- // by `event.type` to fold the wire-frame-equivalent handler
426
- // (render/props_update/etc.) — same cursor model as the HTTP
427
- // `/api/sessions/:id/events?sinceSequence=N` endpoint.
428
- //
429
- // Each replay frame carries `resumeId` = the ledger seq — the SSE
430
- // sink stamps it as the `id:` field so the browser's Last-Event-ID
431
- // lands on the exact same cursor space; the WS sink ignores it.
462
+ // From here the subscriber is registered, so the live pump and the
463
+ // direct walks can write to it; the gate holds their frames until the
464
+ // ack and every replay frame below are written (ggui#1525), and the
465
+ // `finally` always releases them.
432
466
  //
433
- // Pagination: a hasMore-loop over 100-event pages (the HTTP
434
- // route's default page size). Strict superset of the previous
435
- // single-page-100 WS behavior.
436
- //
437
- // Horizon gate: a cursor below the server's replay horizon OR
438
- // above `lastSequence` (stale from a different deployment) emits
439
- // an error frame with `code: 'REPLAY_HORIZON_PASSED'` and skips
440
- // the replay. The frame carries `resumeId` = lastSequence — a
441
- // dispatched id-bearing frame advances the browser's Last-Event-ID
442
- // to the fresh high-water mark, and the ack snapshot already
443
- // re-mounted state. Client recovery: re-mount from a fresh /state
444
- // read (WS) / the already-delivered ack (SSE).
445
- if (args.sinceSequence !== undefined) {
446
- const sinceSeq = args.sinceSequence;
447
- if (sinceSeq < 0 || !Number.isInteger(sinceSeq)) {
448
- sink.write({
449
- type: "error",
450
- payload: {
451
- code: "INVALID_SINCE_SEQUENCE",
452
- message: "sinceSequence must be a non-negative integer",
453
- },
454
- ...(args.requestId ? { requestId: args.requestId } : {}),
455
- });
456
- }
457
- else {
458
- let cursor = sinceSeq;
459
- let firstPage = true;
460
- for (;;) {
461
- const ledger = await deps.renderStore.listEventsSince(stored.id, cursor, 100);
462
- if (ledger === null) {
463
- // GguiSession disappeared between resolve and ledger read —
464
- // already handled by the broader error envelope path;
465
- // nothing to do here.
466
- break;
467
- }
468
- if (firstPage && (sinceSeq > ledger.lastSequence || sinceSeq < ledger.horizonSeq)) {
469
- sink.write({
470
- type: "error",
471
- payload: {
472
- code: "REPLAY_HORIZON_PASSED",
473
- message: `cursor ${sinceSeq} is outside replayable range [${ledger.horizonSeq}, ${ledger.lastSequence}]`,
474
- details: { currentSequence: ledger.lastSequence },
475
- },
476
- ...(args.requestId ? { requestId: args.requestId } : {}),
477
- }, { resumeId: String(ledger.lastSequence) });
478
- break;
479
- }
480
- firstPage = false;
481
- for (const event of ledger.events) {
482
- // GguiSessionEvent is now the wire-shape ledger primitive
483
- // (Wave 7 of flatten-render-identity, 2026-05-28); no
484
- // projection — emit the store's row directly.
485
- sink.write({ type: "render_event", payload: event }, { resumeId: String(event.seq) });
486
- cursor = event.seq;
467
+ // A subscribe that throws past this point (a ledger read that fails)
468
+ // is not left registered (ggui#1528). The client may already hold the
469
+ // ack and would run on a partial replay with no signal, and on SSE
470
+ // nothing else would ever unregister it. So the subscriber is
471
+ // unregistered and its transport ended with `internal_error` (WS
472
+ // close 1011; the SSE stream ends), the client re-subscribes with a
473
+ // fresh replay, and the error still reaches the caller. Not
474
+ // `service_restart`: its 1012 invites an immediate reconnect, and a
475
+ // failure that repeats for one render (a row the store cannot read)
476
+ // would become a tight reconnect loop per open view.
477
+ try {
478
+ // The first ledger page is read BEFORE the ack (ggui#1528). It is the
479
+ // read that can fail for a row the store cannot read, and failing
480
+ // before the ack means the client gets no ack for a subscribe that
481
+ // could not complete: a client whose retry budget resets on an
482
+ // accepted ack then counts the attempt as failed and backs off to
483
+ // its cap, instead of reconnecting once a second forever. Only the
484
+ // first page: every SSE open carries `sinceSequence`, often 0, so
485
+ // reading every page first would hold a render's whole ledger in
486
+ // memory per open. Later pages stream after the ack as before.
487
+ const sinceSeqValid = args.sinceSequence !== undefined && Number.isInteger(args.sinceSequence) && args.sinceSequence >= 0
488
+ ? args.sinceSequence
489
+ : undefined;
490
+ const firstLedgerPage = sinceSeqValid !== undefined ? await deps.renderStore.listEventsSince(stored.id, sinceSeqValid, 100) : undefined;
491
+ const resumeTruncated = args.fromSeq !== undefined && (replay?.truncated === true || counterRestarted);
492
+ deps.logger.info("render_channel_subscribed", {
493
+ sessionId: stored.id,
494
+ appId: stored.appId,
495
+ identityKind: args.identity.identity.kind,
496
+ transport: args.transport,
497
+ fromSeq: args.fromSeq,
498
+ snapshotSeq,
499
+ replayCount: replay?.envelopes.length ?? 0,
500
+ replayTruncated: resumeTruncated,
501
+ ...(counterRestarted ? { counterRestarted: true } : {}),
502
+ // The credential the subscribe came on (ggui#1488): a wsToken, the
503
+ // console cookie, or the adapter's bearer source. The first two ride
504
+ // a synthetic identity whose own `source` would misreport them.
505
+ bootstrap: args.credential === "ws_token",
506
+ source: args.credential ?? args.identity.source,
507
+ });
508
+ const ackPayload = {
509
+ sequence: stored.eventSequence,
510
+ timestamp: Date.now(),
511
+ session: stored.render,
512
+ streamSeq: snapshotSeq,
513
+ // The epoch `streamSeq` counts in (ggui#1531), when the buffer has one.
514
+ ...(snapshot.epoch !== undefined ? { streamEpoch: snapshot.epoch } : {}),
515
+ // Advertise the server's protocol version on every successful
516
+ // subscribe ack (SPEC §11.2.2). Clients whose
517
+ // CLIENT_SUPPORTED_VERSIONS doesn't contain this string surface
518
+ // UpgradeRequiredError to their caller; clients that don't wire
519
+ // the handshake ignore the field (legacy-pass-through).
520
+ serverVersion: PROTOCOL_SCHEMA_VERSION,
521
+ // Only a resume can have a gap: `replayTruncated` is "absent on
522
+ // fresh subscribes" (`AckPayload`). A fresh subscribe still walks the
523
+ // reserved channels from 0, and a buffer whose retention expired
524
+ // reports that walk truncated, which says nothing to this client.
525
+ ...(resumeTruncated ? { replayTruncated: true } : {}),
526
+ };
527
+ sink.write({
528
+ type: "ack",
529
+ payload: ackPayload,
530
+ ...(args.requestId ? { requestId: args.requestId } : {}),
531
+ });
532
+ // R7 — GguiSessionEvent ledger replay. When `args.sinceSequence` is
533
+ // present, fetch events with `seq > sinceSequence` from the per-
534
+ // render ledger and emit each as a `render_event` wire frame
535
+ // BEFORE the per-channel stream-buffer replay. Consumers dispatch
536
+ // by `event.type` to fold the wire-frame-equivalent handler
537
+ // (render/props_update/etc.) — same cursor model as the HTTP
538
+ // `/api/sessions/:id/events?sinceSequence=N` endpoint.
539
+ //
540
+ // Each replay frame carries `resumeId` = the ledger seq — the SSE
541
+ // sink stamps it as the `id:` field so the browser's Last-Event-ID
542
+ // lands on the exact same cursor space; the WS sink ignores it.
543
+ //
544
+ // Pagination: a hasMore-loop over 100-event pages (the HTTP
545
+ // route's default page size). Strict superset of the previous
546
+ // single-page-100 WS behavior.
547
+ //
548
+ // Horizon gate: a cursor below the server's replay horizon OR
549
+ // above `lastSequence` (stale from a different deployment) emits
550
+ // an error frame with `code: 'REPLAY_HORIZON_PASSED'` and skips
551
+ // the replay. The frame carries `resumeId` = lastSequence — a
552
+ // dispatched id-bearing frame advances the browser's Last-Event-ID
553
+ // to the fresh high-water mark, and the ack snapshot already
554
+ // re-mounted state. Client recovery: re-mount from a fresh /state
555
+ // read (WS) / the already-delivered ack (SSE).
556
+ if (args.sinceSequence !== undefined) {
557
+ if (sinceSeqValid === undefined) {
558
+ sink.write({
559
+ type: "error",
560
+ payload: {
561
+ code: "INVALID_SINCE_SEQUENCE",
562
+ message: "sinceSequence must be a non-negative integer",
563
+ },
564
+ ...(args.requestId ? { requestId: args.requestId } : {}),
565
+ });
566
+ }
567
+ else {
568
+ const sinceSeq = sinceSeqValid;
569
+ let cursor = sinceSeq;
570
+ let firstPage = true;
571
+ for (;;) {
572
+ // The first page was read before the ack; later pages are read here.
573
+ const ledger = firstPage
574
+ ? firstLedgerPage
575
+ : await deps.renderStore.listEventsSince(stored.id, cursor, 100);
576
+ if (ledger === null || ledger === undefined) {
577
+ // GguiSession disappeared between resolve and ledger read —
578
+ // already handled by the broader error envelope path;
579
+ // nothing to do here.
580
+ break;
581
+ }
582
+ if (firstPage && (sinceSeq > ledger.lastSequence || sinceSeq < ledger.horizonSeq)) {
583
+ sink.write({
584
+ type: "error",
585
+ payload: {
586
+ code: "REPLAY_HORIZON_PASSED",
587
+ message: `cursor ${sinceSeq} is outside replayable range [${ledger.horizonSeq}, ${ledger.lastSequence}]`,
588
+ details: { currentSequence: ledger.lastSequence },
589
+ },
590
+ ...(args.requestId ? { requestId: args.requestId } : {}),
591
+ }, { resumeId: String(ledger.lastSequence) });
592
+ break;
593
+ }
594
+ firstPage = false;
595
+ for (const event of ledger.events) {
596
+ // GguiSessionEvent is now the wire-shape ledger primitive
597
+ // (Wave 7 of flatten-render-identity, 2026-05-28); no
598
+ // projection — emit the store's row directly.
599
+ sink.write({ type: "render_event", payload: event }, { resumeId: String(event.seq) });
600
+ cursor = event.seq;
601
+ }
602
+ // Defensive: an empty page with hasMore would loop forever;
603
+ // the store contract never produces it, but a broken adapter
604
+ // must not spin the server.
605
+ if (!ledger.hasMore || ledger.events.length === 0)
606
+ break;
487
607
  }
488
- // Defensive: an empty page with hasMore would loop forever;
489
- // the store contract never produces it, but a broken adapter
490
- // must not spin the server.
491
- if (!ledger.hasMore || ledger.events.length === 0)
492
- break;
493
608
  }
494
609
  }
495
- }
496
- // Send replay frames AFTER the ack. Ordering by `seq` ASC — the
497
- // buffer returns them pre-sorted. Client sees ack(streamSeq=N) →
498
- // up to N replay `data` frames → live tail (seq > N). No explicit
499
- // "replay end" marker is needed; the client uses envelope.seq as
500
- // the single source of truth for ordering.
501
- if (replay) {
502
- for (const env of replay.envelopes) {
503
- sink.write({ type: "data", payload: env });
610
+ // Send replay frames AFTER the ack. Ordering by `seq` ASC — the
611
+ // buffer returns them pre-sorted. Client sees ack(streamSeq=N) →
612
+ // up to N replay `data` frames → live tail (seq > N). No explicit
613
+ // "replay end" marker is needed; the client uses envelope.seq as
614
+ // the single source of truth for ordering.
615
+ if (replay) {
616
+ for (const env of replay.envelopes) {
617
+ sink.write({ type: "data", payload: env });
618
+ }
504
619
  }
505
620
  }
621
+ catch (err) {
622
+ deps.unregister(sub);
623
+ sink.end("internal_error");
624
+ throw err;
625
+ }
626
+ finally {
627
+ gate.release();
628
+ }
506
629
  return {
507
630
  detach: () => {
508
631
  deps.unregister(sub);
@@ -1 +1 @@
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
+ {"version":3,"file":"subscriber-lifecycle.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscriber-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,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,CA0H5F"}
@@ -17,6 +17,7 @@
17
17
  * hosted cross-replica pubsub scoping keys on "does this replica hold any
18
18
  * subscriber for this render", regardless of transport.
19
19
  */
20
+ import { admitLiveEnvelope } from './stream-epoch.js';
20
21
  export function createSubscriberLifecycle(deps) {
21
22
  /**
22
23
  * Per-render local subscriber count. Drives the
@@ -35,12 +36,12 @@ export function createSubscriberLifecycle(deps) {
35
36
  * frames with `seq <= replayCompletedSeq` were (or will be)
36
37
  * delivered via the replay path on subscribe.
37
38
  *
38
- * The pump's first action is `await iter.next()`, which yields
39
- * control back to the event loop. This is what preserves the
40
- * subscribe-handler ordering invariant: ack → replay frames →
41
- * live frames. The replay-frame send loop completes synchronously
42
- * before the pump can ever send anything, regardless of fanout
43
- * timing.
39
+ * The ordering invariant ack → replay frames → live frames is kept by
40
+ * the subscribe tail, not by timing: `sub.sink` is gated until the
41
+ * tail has written the ack and every replay frame, so whatever the
42
+ * pump writes meanwhile is held and flushed after them. The tail is
43
+ * NOT synchronous when `sinceSequence` is set (it awaits the ledger
44
+ * read), which is why the gate exists (ggui#1525).
44
45
  */
45
46
  async function pumpSubscriber(sub) {
46
47
  try {
@@ -48,7 +49,16 @@ export function createSubscriberLifecycle(deps) {
48
49
  const { value, done } = await sub.iter.next();
49
50
  if (done)
50
51
  return;
51
- if (value.seq <= sub.replayCompletedSeq)
52
+ const admission = admitLiveEnvelope(sub.replayCursor, value);
53
+ if (admission.epochChanged !== undefined) {
54
+ deps.logger.info("stream_epoch_changed", {
55
+ sessionId: sub.sessionId,
56
+ from: admission.epochChanged.from,
57
+ to: admission.epochChanged.to,
58
+ transport: sub.transport,
59
+ });
60
+ }
61
+ if (!admission.deliver)
52
62
  continue;
53
63
  if (!sub.sink.isOpen()) {
54
64
  await sub.iter.return?.();
@@ -59,7 +59,7 @@ import type { IncomingMessage } from "node:http";
59
59
  import type { Duplex } from "node:stream";
60
60
  import { type GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
61
61
  import type { SubscriberSink } from "./ggui-session-channel/internal-types.js";
62
- import { type GguiSessionChannelBootstrap, type GguiSessionChannelCookieAuth } from "./ggui-session-channel/subscribe.js";
62
+ import { type GguiSessionChannelBootstrap, type GguiSessionChannelCookieAuth, type SubscribeCredential } from "./ggui-session-channel/subscribe.js";
63
63
  import type { Logger } from "./logger.js";
64
64
  /** Default URL path for the channel endpoint. Operators can override. */
65
65
  export declare const DEFAULT_RENDER_CHANNEL_PATH = "/ws";
@@ -112,7 +112,7 @@ export declare const DEFAULT_PRE_SUBSCRIBE_IDLE_MS = 30000;
112
112
  */
113
113
  export declare const DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS = 1024;
114
114
  export type { GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
115
- export type { GguiSessionChannelBootstrap, GguiSessionChannelBootstrapVerifyResult, GguiSessionChannelCookieAuth, } from "./ggui-session-channel/subscribe.js";
115
+ export type { GguiSessionChannelBootstrap, GguiSessionChannelBootstrapVerifyResult, GguiSessionChannelCookieAuth, SubscribeCredential, } from "./ggui-session-channel/subscribe.js";
116
116
  export type { SubscriberSink } from "./ggui-session-channel/internal-types.js";
117
117
  /**
118
118
  * Thrown by {@link GguiSessionChannelServer.attachExternalSubscriber}
@@ -223,9 +223,9 @@ export interface GguiSessionChannelOptions {
223
223
  readonly streamFanout?: StreamFanout;
224
224
  /**
225
225
  * Optional bootstrap-auth plumbing. When present, the channel
226
- * accepts `SubscribePayload.wsToken` and issues reconnect
227
- * credentials in `AckPayload.sessionToken`. When absent, bootstrap
228
- * tokens are rejected with `BOOTSTRAP_NOT_SUPPORTED`.
226
+ * accepts `SubscribePayload.wsToken`; it mints no reconnect
227
+ * credential (ggui#1488). When absent, bootstrap tokens are rejected
228
+ * with `BOOTSTRAP_NOT_SUPPORTED`.
229
229
  */
230
230
  readonly bootstrap?: GguiSessionChannelBootstrap;
231
231
  /**
@@ -580,6 +580,14 @@ export interface GguiSessionChannelServer {
580
580
  readonly sink: SubscriberSink;
581
581
  readonly sinceSequence?: number;
582
582
  readonly fromSeq?: number;
583
+ /** The epoch `fromSeq` was counted in (ggui#1531); read only beside it. */
584
+ readonly fromEpoch?: string;
585
+ /**
586
+ * The credential the transport authenticated (ggui#1488), logged as
587
+ * `render_channel_subscribed.source`; the SSE route passes
588
+ * `"ws_token"`. Absent: the identity's own auth source.
589
+ */
590
+ readonly credential?: SubscribeCredential;
583
591
  }): Promise<{
584
592
  readonly detach: () => void;
585
593
  }>;
@@ -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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"}
@@ -204,7 +204,6 @@ export function createGguiSessionChannelServer(opts) {
204
204
  renderStore: opts.renderStore,
205
205
  localTools: opts.streamWebSocketLocalTools,
206
206
  subscribersByWs,
207
- send,
208
207
  sendChannelError,
209
208
  });
210
209
  // Action ingress + consume bridge — contract gate, ledger + pipe
@@ -312,6 +311,8 @@ export function createGguiSessionChannelServer(opts) {
312
311
  transport: "sse",
313
312
  ...(args.sinceSequence !== undefined ? { sinceSequence: args.sinceSequence } : {}),
314
313
  ...(args.fromSeq !== undefined ? { fromSeq: args.fromSeq } : {}),
314
+ ...(args.fromEpoch !== undefined ? { fromEpoch: args.fromEpoch } : {}),
315
+ ...(args.credential !== undefined ? { credential: args.credential } : {}),
315
316
  });
316
317
  },
317
318
  get subscriberCount() {
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export type { HandlerContext, SharedHandler } from '@ggui-ai/mcp-server-handlers
29
29
  export type { GadgetDescriptor, McpUiDisplayMode, GguiSession, SystemGguiSession, } from '@ggui-ai/protocol';
30
30
  export type { GenerationDeps } from '@ggui-ai/mcp-server-handlers';
31
31
  export { buildNoCredentialsGguiSession } from '@ggui-ai/mcp-server-handlers';
32
- export { buildOpsBundleHandlers, createGguiServer, defaultHandlers } from './server.js';
32
+ export { buildOpsBundleHandlers, createGguiServer, defaultHandlers, DEFAULT_CLOSE_GRACE_MS } from './server.js';
33
33
  export type { CreateGguiServerOptions, ErrorMapperResult, GguiServer, OpsBundleDeps, } from './server.js';
34
34
  export { AnthropicStructuredCallError, buildLlmCaller } from './llm-backed-negotiator.js';
35
35
  export type { AnthropicStructuredCallFailureKind, BuildLlmCallerOptions, } from './llm-backed-negotiator.js';