@ggui-ai/mcp-server 0.24.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 (70) 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 +82 -12
  11. package/dist/console-auth.d.ts +10 -10
  12. package/dist/console-auth.d.ts.map +1 -1
  13. package/dist/control-service.d.ts.map +1 -1
  14. package/dist/control-service.js +0 -3
  15. package/dist/first-seen.d.ts +14 -0
  16. package/dist/first-seen.d.ts.map +1 -0
  17. package/dist/first-seen.js +19 -0
  18. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  19. package/dist/ggui-session-channel/action-ingress.js +96 -8
  20. package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
  21. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  22. package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
  23. package/dist/ggui-session-channel/internal-types.d.ts +20 -12
  24. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  26. package/dist/ggui-session-channel/outbound.js +18 -6
  27. package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
  28. package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
  29. package/dist/ggui-session-channel/stream-epoch.js +47 -0
  30. package/dist/ggui-session-channel/subscribe.d.ts +40 -53
  31. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/subscribe.js +288 -124
  33. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  34. package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
  35. package/dist/ggui-session-channel.d.ts +43 -7
  36. package/dist/ggui-session-channel.d.ts.map +1 -1
  37. package/dist/ggui-session-channel.js +7 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/instructions-presets.d.ts +22 -21
  42. package/dist/instructions-presets.d.ts.map +1 -1
  43. package/dist/instructions-presets.js +38 -38
  44. package/dist/llm-backed-negotiator.d.ts +4 -37
  45. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  46. package/dist/llm-backed-negotiator.js +28 -210
  47. package/dist/mcp-apps-outbound.d.ts +40 -91
  48. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  49. package/dist/mcp-apps-outbound.js +100 -15
  50. package/dist/mcp-endpoint-routes.d.ts +1 -0
  51. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  52. package/dist/mcp-endpoint-routes.js +38 -4
  53. package/dist/runtime-bundle-hash.d.ts +7 -0
  54. package/dist/runtime-bundle-hash.d.ts.map +1 -1
  55. package/dist/runtime-bundle-hash.js +8 -2
  56. package/dist/runtime-bundle-route.d.ts.map +1 -1
  57. package/dist/runtime-bundle-route.js +15 -0
  58. package/dist/server.d.ts +143 -85
  59. package/dist/server.d.ts.map +1 -1
  60. package/dist/server.js +224 -110
  61. package/dist/storage.d.ts +9 -1
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +4 -3
  64. package/dist/view-proof-gate.d.ts +96 -0
  65. package/dist/view-proof-gate.d.ts.map +1 -0
  66. package/dist/view-proof-gate.js +167 -0
  67. package/package.json +14 -15
  68. package/dist/render-read-gate.d.ts +0 -50
  69. package/dist/render-read-gate.d.ts.map +0 -1
  70. package/dist/render-read-gate.js +0 -36
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Embedded-ui cookie auth plane.
3
3
  *
4
- * This module owns the THIRD of the three token kinds the server
5
- * tracks for the embedded UI:
4
+ * This module owns the second of the two token kinds the server mints
5
+ * for the embedded UI (a third, the reconnect "session" token, is minted
6
+ * by no server since ggui#1488):
6
7
  *
7
- * 1. bootstrap tokens — short-TTL, single-use, MCP Apps iframes.
8
- * 2. session tokens — longer-TTL, reusable, post-bootstrap
9
- * reconnect creds.
10
- * 3. console cookies — longer-TTL, reusable, same-origin
8
+ * 1. ws (bootstrap) tokens — short-TTL, reusable within TTL, MCP Apps
9
+ * iframes.
10
+ * 2. console cookies — longer-TTL, reusable, same-origin
11
11
  * browser-only, issued by this server
12
12
  * for ITS OWN console landing/viewer
13
13
  * pages. **Scoped narrowly**: consumed
@@ -17,10 +17,10 @@
17
17
  * `/threads`, or any other ingress.
18
18
  *
19
19
  * Isolation invariant (load-bearing). The cookie uses the SAME HMAC
20
- * shape as the bootstrap/session tokens but a distinct `kind` claim
20
+ * shape as the ws (bootstrap) token but a distinct `kind` claim
21
21
  * (`'console-session'`). That makes cross-kind confusion
22
- * impossible: a cookie value CAN'T verify as a bootstrap or session
23
- * token. Secrets are shared across kinds because they live in the
22
+ * impossible: a cookie value CAN'T verify as a ws token, nor any
23
+ * other kind as a cookie. Secrets are shared across kinds because they live in the
24
24
  * same trust domain (server-minted same-origin creds) and kind
25
25
  * discrimination is sufficient.
26
26
  *
@@ -63,7 +63,7 @@ export interface DevtoolCookieMint {
63
63
  export interface MintDevtoolCookieInput {
64
64
  readonly sessionId: string;
65
65
  readonly appId: string;
66
- /** HMAC secret. Shared with bootstrap + session tokens by design. */
66
+ /** HMAC secret. Shared with the ws (bootstrap) tokens by design. */
67
67
  readonly secret: string;
68
68
  /** Cookie TTL in seconds. Defaults to 8 hours. */
69
69
  readonly ttlSec?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAqBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,mBAAmB,GAAG,IAAI,CAO5B"}
1
+ {"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAqBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,mBAAmB,GAAG,IAAI,CAO5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAK,KAAK,WAAW,EAAmB,MAAM,KAAK,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAiB9C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC/G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAGxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC9G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAYxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EACjH,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAkCzC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,cAAc,CAwBjF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C"}
1
+ {"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAK,KAAK,WAAW,EAAmB,MAAM,KAAK,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAc9C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC/G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAGxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC9G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAYxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EACjH,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAkCzC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,cAAc,CAwBjF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C"}
@@ -105,15 +105,12 @@ export function filterHandlersByAudience(set, allowed) {
105
105
  export const SINGLE_CALL_OPS = new Set([
106
106
  "ggui_ops_get_credit_balance",
107
107
  "ggui_ops_get_my_blueprint_source",
108
- "ggui_ops_get_org_balance",
109
108
  "ggui_ops_get_render_source",
110
109
  "ggui_ops_list_apps",
111
110
  "ggui_ops_list_blueprints",
112
- "ggui_ops_list_connector_keys",
113
111
  "ggui_ops_list_credit_transactions",
114
112
  "ggui_ops_list_my_apps",
115
113
  "ggui_ops_list_my_blueprints",
116
- "ggui_ops_list_orgs",
117
114
  "ggui_ops_list_provider_keys",
118
115
  "ggui_ops_list_themes",
119
116
  "ggui_ops_list_recent_renders",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A bounded "seen before?" set, for a log line that should fire once per key
3
+ * per server process without growing with the number of keys.
4
+ *
5
+ * Insertion-ordered: at capacity, the oldest key is forgotten, so a key
6
+ * offered again after `capacity` newer keys counts as new and its line fires
7
+ * a second time. A reader of such a line takes the earliest per key.
8
+ */
9
+ export interface FirstSeen {
10
+ /** True the first time `key` is offered (or once it has been forgotten); false after. */
11
+ first(key: string): boolean;
12
+ }
13
+ export declare function createFirstSeen(capacity: number): FirstSeen;
14
+ //# sourceMappingURL=first-seen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"first-seen.d.ts","sourceRoot":"","sources":["../src/first-seen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,yFAAyF;IACzF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAgB3D"}
@@ -0,0 +1,19 @@
1
+ export function createFirstSeen(capacity) {
2
+ if (!Number.isInteger(capacity) || capacity < 1) {
3
+ throw new RangeError(`capacity must be a positive integer, got ${capacity}`);
4
+ }
5
+ const seen = new Set();
6
+ return {
7
+ first(key) {
8
+ if (seen.has(key))
9
+ return false;
10
+ if (seen.size >= capacity) {
11
+ const oldest = seen.values().next();
12
+ if (!oldest.done)
13
+ seen.delete(oldest.value);
14
+ }
15
+ seen.add(key);
16
+ return true;
17
+ },
18
+ };
19
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAOvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AA+G9C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,mBAAmB,CACjB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA+K1E"}
1
+ {"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAEhB,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AA+G9C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,mBAAmB,CACjB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA6O1E"}
@@ -177,13 +177,63 @@ export function createActionIngress(deps) {
177
177
  // reaped) degrades to ledger-only with a warn — the WS client has
178
178
  // no `ui/message` fallback to branch on, so a new error frame
179
179
  // would be vocabulary without a consumer.
180
+ //
181
+ // The append's outcome (ggui#1517) is not read here: this path mints
182
+ // the pipe id per frame (`toConsumeEventEntry`), so a duplicate cannot
183
+ // arise, and the ledger write is this path's load-bearing one.
184
+ // The pipe entry (and its minted actionId, which also names the holder of
185
+ // a oneShot spend, ggui#1424) is built once per frame.
186
+ const entry = envelope.type === "data:submit" ? toConsumeEventEntry(envelope, sub.sessionId) : null;
187
+ // ggui#1424 — the channel door refuses a second gesture on a spent
188
+ // oneShot too, so the server's refusal holds at both doors. The claim is
189
+ // taken BEFORE the writes under this frame's minted actionId; the answer
190
+ // decides as it does on the tool door (`claimDispatchSpend` there):
191
+ // already-spent by another holder whose delivery is marked → refused with
192
+ // the channel's existing CONTRACT_VIOLATION error frame (#1358), one
193
+ // violation at `actionSpec.<name>.oneShot`, no ledger row, no pipe entry,
194
+ // one named line; an unmarked holder is taken over; the same holder, no
195
+ // holder, a non-oneShot action, a store that answers no claim, or a store
196
+ // failure (fail-open, named) → the frame goes on as before. The store
197
+ // enforces `delivered`: the refusal arm reads it only as the store
198
+ // answers it. Delivery is marked after the writes below.
199
+ const framePayload = envelope.payload;
200
+ const oneShotPayload = entry !== null &&
201
+ activeItem !== undefined &&
202
+ framePayload !== null &&
203
+ typeof framePayload === "object" &&
204
+ !Array.isArray(framePayload) &&
205
+ typeof framePayload.action === "string"
206
+ ? { action: framePayload.action, data: framePayload.data }
207
+ : null;
208
+ if (oneShotPayload !== null && entry !== null && activeItem !== undefined) {
209
+ const verdict = await claimChannelSpend(deps, sub.sessionId, activeItem, oneShotPayload, entry.actionId);
210
+ if (verdict.kind === "refuse") {
211
+ deps.logger.warn("render_channel_one_shot_refused", {
212
+ sessionId: sub.sessionId,
213
+ action: oneShotPayload.action,
214
+ actionId: entry.actionId,
215
+ by: verdict.by,
216
+ });
217
+ deps.sendError(ws, "CONTRACT_VIOLATION", `actionSpec.${oneShotPayload.action} is declared oneShot and already fired on this card; a second gesture is refused`, message.requestId, {
218
+ error: "contract_violation",
219
+ violations: [
220
+ {
221
+ field: `actionSpec.${oneShotPayload.action}.oneShot`,
222
+ keyword: "oneShot",
223
+ message: "this one-shot action already fired on this card",
224
+ expected: verdict.by,
225
+ received: entry.actionId,
226
+ },
227
+ ],
228
+ hint: "The action already fired on this card. Render a new card to fire it again.",
229
+ });
230
+ return;
231
+ }
232
+ }
180
233
  const consumeWrite = (() => {
181
- if (deps.pendingEventConsumer === undefined || envelope.type !== "data:submit") {
234
+ if (deps.pendingEventConsumer === undefined || entry === null) {
182
235
  return Promise.resolve();
183
236
  }
184
- const entry = toConsumeEventEntry(envelope, sub.sessionId);
185
- if (entry === null)
186
- return Promise.resolve();
187
237
  return deps.pendingEventConsumer.append(sub.sessionId, {
188
238
  // The pipe entry's stable id doubles as the `drain_ack` key —
189
239
  // same convention as the relay path's iframe-supplied id.
@@ -218,10 +268,12 @@ export function createActionIngress(deps) {
218
268
  return;
219
269
  }
220
270
  const seq = ledgerResult.value;
221
- // ggui#1223 / #1305 — the gesture is in the ledger, so a committed
222
- // `oneShot` now spends its card durably, BEFORE the ack: a client that
223
- // holds the ack can reload and find the card spent. FAIL-OPEN, as on the
224
- // tool path: a failed record is named and never withholds the ack.
271
+ // ggui#1223 / #1305 / #1424 — the gesture is in the ledger, so the
272
+ // holder MARKS its spend delivered (claimed above, before the writes),
273
+ // BEFORE the ack: a client that holds the ack can reload and find the
274
+ // card spent. FAIL-OPEN, as on the tool path: a failed record is named
275
+ // and never withholds the ack. With no pipe entry (the frame did not
276
+ // parse as a dispatch) the spend is recorded held by nobody, as before.
225
277
  if (envelope.type === "data:submit" && activeItem !== undefined) {
226
278
  const payload = envelope.payload;
227
279
  if (payload !== null &&
@@ -235,6 +287,7 @@ export function createActionIngress(deps) {
235
287
  render: activeItem,
236
288
  action: payload.action,
237
289
  data: payload.data,
290
+ ...(entry !== null ? { actionId: entry.actionId, delivered: true } : {}),
238
291
  });
239
292
  }
240
293
  catch (err) {
@@ -254,3 +307,38 @@ export function createActionIngress(deps) {
254
307
  }
255
308
  return { handleInboundAction };
256
309
  }
310
+ async function claimChannelSpend(deps, sessionId, render, payload, actionId) {
311
+ const claimOnce = async (spend) => {
312
+ try {
313
+ return await recordCommittedOneShot({
314
+ store: deps.renderStore,
315
+ sessionId,
316
+ render,
317
+ action: payload.action,
318
+ data: payload.data,
319
+ ...spend,
320
+ });
321
+ }
322
+ catch (err) {
323
+ deps.logger.warn("render_channel_spent_oneshot_persist_failed", {
324
+ sessionId,
325
+ action: payload.action,
326
+ error: err instanceof Error ? err.message : String(err),
327
+ });
328
+ return undefined;
329
+ }
330
+ };
331
+ const first = await claimOnce({ actionId });
332
+ const claim = first?.claim;
333
+ if (claim === undefined || claim.outcome !== "already-spent" || claim.by === undefined || claim.by === actionId) {
334
+ return { kind: "proceed" };
335
+ }
336
+ if (claim.delivered === true)
337
+ return { kind: "refuse", by: claim.by };
338
+ const retaken = await claimOnce({ actionId, reclaimFrom: claim.by });
339
+ const second = retaken?.claim;
340
+ if (second === undefined || second.outcome !== "already-spent" || second.by === undefined || second.by === actionId) {
341
+ return { kind: "proceed" };
342
+ }
343
+ return { kind: "refuse", by: second.by };
344
+ }
@@ -69,7 +69,6 @@ export interface ChannelSubscriptionsDeps {
69
69
  * subscriber.
70
70
  */
71
71
  readonly subscribersByWs: WeakMap<WebSocket, WsSubscriber>;
72
- readonly send: Outbound["send"];
73
72
  readonly sendChannelError: Outbound["sendChannelError"];
74
73
  }
75
74
  export interface ChannelSubscriptions {
@@ -1 +1 @@
1
- {"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,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,EAA4B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAa9C;;;;;;;GAOG;AACH,MAAM,WAAW,mCAAmC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,mCAAmC,GAAG,SAAS,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,sBAAsB,CACpB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;OAKG;IACH,wBAAwB,CACtB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,qBAAqB,CAAA;KAAE,GAC1D,IAAI,CAAC;CACT;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAiQ/F"}
1
+ {"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,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,EAA4B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAa9C;;;;;;;GAOG;AACH,MAAM,WAAW,mCAAmC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,mCAAmC,GAAG,SAAS,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,sBAAsB,CACpB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;OAKG;IACH,wBAAwB,CACtB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,qBAAqB,CAAA;KAAE,GAC1D,IAAI,CAAC;CACT;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAoQ/F"}
@@ -68,7 +68,10 @@ export function createChannelSubscriptions(deps) {
68
68
  if (sub.ws.readyState !== sub.ws.OPEN)
69
69
  return;
70
70
  state.seq += 1;
71
- deps.send(sub.ws, {
71
+ // Through the subscriber's sink, like every data-carrying writer,
72
+ // so a poll that lands while the subscribe tail is still writing
73
+ // its replay is held until after it (ggui#1525).
74
+ sub.sink.write({
72
75
  type: "channel_payload",
73
76
  payload: {
74
77
  sessionId: sub.sessionId,
@@ -3,6 +3,7 @@
3
3
  * of the package's public surface — everything publishable is exported
4
4
  * (or re-exported) from `../ggui-session-channel.ts`.
5
5
  */
6
+ import type { StreamReplayCursor } from './stream-epoch.js';
6
7
  import type { AuthResult, BufferedStreamEnvelope } from "@ggui-ai/mcp-server-core";
7
8
  import type { JsonObject } from "@ggui-ai/protocol";
8
9
  import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
@@ -30,12 +31,16 @@ export interface SubscriberSink {
30
31
  }): void;
31
32
  /**
32
33
  * Terminate the transport. WS maps `service_restart` → close 1012
33
- * (reconnect immediately — same server family is still up) and
34
+ * (reconnect immediately — same server family is still up),
34
35
  * `session_expired` → close 1008 (policy violation — do not blind-
35
- * reconnect); SSE ends the response, letting `EventSource`
36
- * auto-reconnect hit the HTTP pre-gate for the authoritative verdict.
36
+ * reconnect), and `internal_error` → close 1011 (the server could not
37
+ * complete the subscribe; a client reconnects with its ordinary
38
+ * backoff, since a failure that may repeat must not invite an
39
+ * immediate retry, ggui#1528). SSE ends the response for every reason,
40
+ * letting `EventSource` auto-reconnect hit the HTTP pre-gate for the
41
+ * authoritative verdict.
37
42
  */
38
- end(reason: "service_restart" | "session_expired"): void;
43
+ end(reason: "service_restart" | "session_expired" | "internal_error"): void;
39
44
  }
40
45
  /**
41
46
  * Fields shared by every subscriber regardless of transport (one
@@ -48,16 +53,19 @@ interface SubscriberBase {
48
53
  readonly identity: AuthResult;
49
54
  readonly connectedAt: number;
50
55
  /**
51
- * Largest outbound `seq` the initial replay (or subscribe snapshot)
52
- * covered for this subscriber. Live fan-out skips envelopes with
53
- * `seq <= replayCompletedSeq` to prevent double delivery — those
54
- * were (or will be) delivered via the replay phase.
56
+ * The replay boundary: the largest outbound `seq` the initial replay
57
+ * (or subscribe snapshot) covered for this subscriber, and the epoch
58
+ * that `seq` counts in (ggui#1531). Live fan-out skips envelopes of the
59
+ * same epoch with `seq <= replayCursor.seq` to prevent double delivery,
60
+ * since those were (or will be) delivered via the replay phase. A live
61
+ * envelope of another epoch means the counter restarted: it is
62
+ * delivered and moves the cursor (`admitLiveEnvelope`).
55
63
  *
56
- * For fresh subscribers (no `fromSeq`), this is the stream cursor
57
- * at subscribe time; they never see the pre-existing buffer, only
58
- * new deliveries.
64
+ * For fresh subscribers (no `fromSeq`), `seq` is the stream cursor at
65
+ * subscribe time; they never see the pre-existing buffer, only new
66
+ * deliveries.
59
67
  */
60
- readonly replayCompletedSeq: number;
68
+ readonly replayCursor: StreamReplayCursor;
61
69
  /**
62
70
  * Per-subscriber live-tail iterator from `streamFanout.subscribe`.
63
71
  * Owned by the subscriber for its full lifetime; ending it (via
@@ -1 +1 @@
1
- {"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,IAAI,CAAC;CAC1D;AAED;;;;GAIG;AACH,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;OASG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IAC5D,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;CAC3B;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,iBAAiB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D"}
1
+ {"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E;;;;;;;;;;OAUG;IACH,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,CAAC;CAC7E;AAED;;;;GAIG;AACH,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IAC5D,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;CAC3B;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,iBAAiB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@
11
11
  * `GguiSessionChannelServer` interface in `../ggui-session-channel.ts`;
12
12
  * inline comments here cover impl-level decisions only.
13
13
  */
14
+ import { admitLiveEnvelope } from "./stream-epoch.js";
14
15
  import { assertStreamContract } from "@ggui-ai/mcp-server-handlers/renders";
15
16
  /**
16
17
  * Low-level ws wire write shared by the closure-bound `send` helper and
@@ -43,7 +44,7 @@ export function createWsSink(ws, logger) {
43
44
  },
44
45
  end: (reason) => {
45
46
  try {
46
- ws.close(reason === "service_restart" ? 1012 : 1008, reason);
47
+ ws.close(reason === "service_restart" ? 1012 : reason === "internal_error" ? 1011 : 1008, reason);
47
48
  }
48
49
  catch {
49
50
  /* best-effort — socket may already be closing */
@@ -204,11 +205,22 @@ export function createOutbound(deps) {
204
205
  // subscribe-replay (policy 'all'/'latest') or is deliberately
205
206
  // invisible to late subscribers (policy 'none') — never re-send.
206
207
  // Unstamped frames were never buffered; replay cannot duplicate
207
- // them, so they pass. Non-data frames carry no seq semantics.
208
- if (frame.type === "data" &&
209
- typeof frame.payload.seq === "number" &&
210
- frame.payload.seq <= sub.replayCompletedSeq) {
211
- continue;
208
+ // them, so they pass. A frame of another epoch means the counter
209
+ // restarted: it passes and moves the subscriber's cursor, by the
210
+ // same rule as the pump (ggui#1531). Non-data frames carry no seq
211
+ // semantics.
212
+ if (frame.type === "data") {
213
+ const admission = admitLiveEnvelope(sub.replayCursor, frame.payload);
214
+ if (admission.epochChanged !== undefined) {
215
+ deps.logger.info("stream_epoch_changed", {
216
+ sessionId,
217
+ from: admission.epochChanged.from,
218
+ to: admission.epochChanged.to,
219
+ transport: sub.transport,
220
+ });
221
+ }
222
+ if (!admission.deliver)
223
+ continue;
212
224
  }
213
225
  sub.sink.write(frame);
214
226
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The live tail's replay boundary, with the stream counter's epoch
3
+ * (ggui#1531). One rule, shared by the two places a live data frame is
4
+ * filtered against a subscriber's replay: the in-process pump
5
+ * (`subscriber-lifecycle.ts`) and the cross-replica direct walk
6
+ * (`outbound.ts`, `externalBroadcast`).
7
+ *
8
+ * A `seq` counts within one counter. When the counter restarts while a
9
+ * subscriber stays attached (a hosted counter evicted or expired, a
10
+ * server restarted over a persistent session store), the next frame
11
+ * arrives at a low `seq` with a new epoch. Filtering it by the old
12
+ * boundary would drop it, and every later one, with no signal: the
13
+ * frame of another epoch is delivered instead, and the subscriber moves
14
+ * to the new epoch with an empty boundary.
15
+ */
16
+ /**
17
+ * A subscriber's replay boundary: the latest `seq` its snapshot and
18
+ * replay covered, and the epoch that `seq` counts in (`undefined` when
19
+ * the snapshot had none: no counter yet, or a buffer that predates
20
+ * epochs). Mutable: a live frame of another epoch moves it.
21
+ */
22
+ export interface StreamReplayCursor {
23
+ seq: number;
24
+ epoch: string | undefined;
25
+ /**
26
+ * The epochs this cursor has moved away from, newest last, at most
27
+ * {@link RETIRED_EPOCHS_KEPT}. A late frame of one of them (a
28
+ * cross-replica frame published just before the restart) is dropped, so
29
+ * it cannot move the cursor back and empty its boundary again.
30
+ */
31
+ retired?: string[];
32
+ }
33
+ /** How many epochs a cursor remembers having left. */
34
+ export declare const RETIRED_EPOCHS_KEPT = 4;
35
+ /** What to do with one live data frame for one subscriber. */
36
+ export interface LiveAdmission {
37
+ readonly deliver: boolean;
38
+ /** Set when the frame moved the subscriber to another epoch. */
39
+ readonly epochChanged?: {
40
+ readonly from: string;
41
+ readonly to: string;
42
+ };
43
+ }
44
+ /**
45
+ * Decide whether a live data frame reaches a subscriber, and move its
46
+ * cursor when the frame's epoch differs:
47
+ * - an unstamped frame (no `seq`) was never buffered and always passes;
48
+ * - a late frame of an epoch the cursor has left is dropped;
49
+ * - a frame of another epoch than the cursor's is delivered, and the
50
+ * cursor moves to that epoch with an empty boundary (a restart);
51
+ * - the first epoch a cursor without one sees is adopted, and its seq
52
+ * boundary stands (the snapshot simply had no epoch yet);
53
+ * - otherwise a frame at or below the boundary was, or will be, sent
54
+ * by the replay, and is skipped.
55
+ * An absent epoch on the frame is unknown, never a mismatch.
56
+ */
57
+ export declare function admitLiveEnvelope(cursor: StreamReplayCursor, envelope: {
58
+ readonly seq?: number;
59
+ readonly streamEpoch?: string;
60
+ }): LiveAdmission;
61
+ //# sourceMappingURL=stream-epoch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-epoch.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/stream-epoch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE,aAAa,CAcf"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The live tail's replay boundary, with the stream counter's epoch
3
+ * (ggui#1531). One rule, shared by the two places a live data frame is
4
+ * filtered against a subscriber's replay: the in-process pump
5
+ * (`subscriber-lifecycle.ts`) and the cross-replica direct walk
6
+ * (`outbound.ts`, `externalBroadcast`).
7
+ *
8
+ * A `seq` counts within one counter. When the counter restarts while a
9
+ * subscriber stays attached (a hosted counter evicted or expired, a
10
+ * server restarted over a persistent session store), the next frame
11
+ * arrives at a low `seq` with a new epoch. Filtering it by the old
12
+ * boundary would drop it, and every later one, with no signal: the
13
+ * frame of another epoch is delivered instead, and the subscriber moves
14
+ * to the new epoch with an empty boundary.
15
+ */
16
+ /** How many epochs a cursor remembers having left. */
17
+ export const RETIRED_EPOCHS_KEPT = 4;
18
+ /**
19
+ * Decide whether a live data frame reaches a subscriber, and move its
20
+ * cursor when the frame's epoch differs:
21
+ * - an unstamped frame (no `seq`) was never buffered and always passes;
22
+ * - a late frame of an epoch the cursor has left is dropped;
23
+ * - a frame of another epoch than the cursor's is delivered, and the
24
+ * cursor moves to that epoch with an empty boundary (a restart);
25
+ * - the first epoch a cursor without one sees is adopted, and its seq
26
+ * boundary stands (the snapshot simply had no epoch yet);
27
+ * - otherwise a frame at or below the boundary was, or will be, sent
28
+ * by the replay, and is skipped.
29
+ * An absent epoch on the frame is unknown, never a mismatch.
30
+ */
31
+ export function admitLiveEnvelope(cursor, envelope) {
32
+ if (envelope.seq === undefined)
33
+ return { deliver: true };
34
+ const epoch = envelope.streamEpoch;
35
+ if (epoch !== undefined && cursor.retired?.includes(epoch) === true)
36
+ return { deliver: false };
37
+ if (epoch !== undefined && epoch !== cursor.epoch) {
38
+ const from = cursor.epoch;
39
+ cursor.epoch = epoch;
40
+ if (from !== undefined) {
41
+ cursor.retired = [...(cursor.retired ?? []), from].slice(-RETIRED_EPOCHS_KEPT);
42
+ cursor.seq = 0;
43
+ return { deliver: true, epochChanged: { from, to: epoch } };
44
+ }
45
+ }
46
+ return { deliver: envelope.seq > cursor.seq };
47
+ }