@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.
- package/README.md +2 -2
- package/dist/api-renders-routes.d.ts +46 -5
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +98 -39
- package/dist/api-renders-stream-route.d.ts +2 -1
- package/dist/api-renders-stream-route.d.ts.map +1 -1
- package/dist/api-renders-stream-route.js +14 -2
- package/dist/build-mcp.d.ts +16 -0
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +82 -12
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +0 -3
- package/dist/first-seen.d.ts +14 -0
- package/dist/first-seen.d.ts.map +1 -0
- package/dist/first-seen.js +19 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/action-ingress.js +96 -8
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
- package/dist/ggui-session-channel/internal-types.d.ts +20 -12
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +18 -6
- package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
- package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
- package/dist/ggui-session-channel/stream-epoch.js +47 -0
- package/dist/ggui-session-channel/subscribe.d.ts +40 -53
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +288 -124
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
- package/dist/ggui-session-channel.d.ts +43 -7
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/instructions-presets.d.ts +22 -21
- package/dist/instructions-presets.d.ts.map +1 -1
- package/dist/instructions-presets.js +38 -38
- package/dist/llm-backed-negotiator.d.ts +4 -37
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +28 -210
- package/dist/mcp-apps-outbound.d.ts +40 -91
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +100 -15
- package/dist/mcp-endpoint-routes.d.ts +1 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +38 -4
- package/dist/runtime-bundle-hash.d.ts +7 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -1
- package/dist/runtime-bundle-hash.js +8 -2
- package/dist/runtime-bundle-route.d.ts.map +1 -1
- package/dist/runtime-bundle-route.js +15 -0
- package/dist/server.d.ts +143 -85
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +224 -110
- package/dist/storage.d.ts +9 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -3
- package/dist/view-proof-gate.d.ts +96 -0
- package/dist/view-proof-gate.d.ts.map +1 -0
- package/dist/view-proof-gate.js +167 -0
- package/package.json +14 -15
- package/dist/render-read-gate.d.ts +0 -50
- package/dist/render-read-gate.d.ts.map +0 -1
- package/dist/render-read-gate.js +0 -36
package/dist/console-auth.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Embedded-ui cookie auth plane.
|
|
3
3
|
*
|
|
4
|
-
* This module owns the
|
|
5
|
-
*
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
23
|
-
*
|
|
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
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/control-service.js
CHANGED
|
@@ -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,
|
|
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 ||
|
|
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
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
// tool path: a failed record is named
|
|
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,
|
|
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
|
-
|
|
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)
|
|
34
|
+
* (reconnect immediately — same server family is still up),
|
|
34
35
|
* `session_expired` → close 1008 (policy violation — do not blind-
|
|
35
|
-
* reconnect)
|
|
36
|
-
*
|
|
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
|
-
*
|
|
52
|
-
* covered for this subscriber
|
|
53
|
-
* `seq
|
|
54
|
-
*
|
|
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`),
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;GAYG;
|
|
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.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|