@ggui-ai/iframe-runtime 0.2.0-alpha.4 → 0.4.0-rc.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/dist/channel-transport.d.ts +20 -30
- package/dist/channel-transport.js +24 -24
- package/dist/channels/data.d.ts +5 -20
- package/dist/channels/data.js +4 -52
- package/dist/channels/index.d.ts +0 -1
- package/dist/channels/index.js +0 -1
- package/dist/channels/props-update.d.ts +9 -9
- package/dist/channels/props-update.js +9 -9
- package/dist/channels/render-frame.d.ts +5 -5
- package/dist/channels/render-frame.js +5 -5
- package/dist/compiled-validators.d.ts +2 -2
- package/dist/compiled-validators.js +1 -1
- package/dist/context-observer.d.ts +4 -4
- package/dist/context-observer.js +3 -3
- package/dist/events-polling.d.ts +7 -7
- package/dist/events-polling.js +5 -5
- package/dist/globals.d.ts +1 -1
- package/dist/host-context-emitter.d.ts +4 -4
- package/dist/host-context-emitter.js +9 -9
- package/dist/iframe-runtime.js +68 -78
- package/dist/iframe-runtime.meta.json +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +29 -6
- package/dist/mcp-app-iframe-host.d.ts +8 -8
- package/dist/mcp-app-iframe-host.js +4 -4
- package/dist/meta-parse.d.ts +3 -4
- package/dist/meta-parse.js +17 -44
- package/dist/observability.d.ts +14 -36
- package/dist/observability.js +2 -1
- package/dist/protocol-error.d.ts +38 -48
- package/dist/protocol-error.js +5 -23
- package/dist/provisional-renderer.js +3 -3
- package/dist/react-renderer.d.ts +16 -6
- package/dist/react-renderer.js +22 -40
- package/dist/registry-subscribe.d.ts +7 -7
- package/dist/registry-subscribe.js +28 -14
- package/dist/render-item.d.ts +22 -15
- package/dist/render-item.js +7 -14
- package/dist/runtime.d.ts +79 -105
- package/dist/runtime.js +219 -269
- package/dist/status-dom.d.ts +12 -12
- package/dist/status-dom.js +11 -11
- package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
- package/dist/system-cards/NoCredentialsCard.js +1 -1
- package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
- package/dist/system-cards/ProtocolProbeCard.js +21 -25
- package/dist/system-cards/index.d.ts +5 -5
- package/dist/system-cards/index.js +5 -5
- package/dist/types.d.ts +29 -34
- package/dist/validation.d.ts +4 -5
- package/dist/validation.js +5 -6
- package/dist/wire-config.d.ts +43 -106
- package/dist/wire-config.js +29 -195
- package/package.json +14 -12
- package/dist/channels/feedback.d.ts +0 -14
- package/dist/channels/feedback.js +0 -19
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* - **Iframe polling** — fallback for every other channel. The
|
|
17
17
|
* router runs a setInterval that fires `tools/call` against
|
|
18
|
-
* the parent MCP host (
|
|
18
|
+
* the parent MCP host (direct, no bridge). Works
|
|
19
19
|
* for any tool the agent has access to, including third-party
|
|
20
20
|
* MCP servers the local pod can't subscribe-for.
|
|
21
21
|
*
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
* fallback (don't double-deliver).
|
|
34
34
|
*
|
|
35
35
|
* The WS-level exponential backoff (1s → 2s → 4s → ... cap 60s) lives
|
|
36
|
-
* on
|
|
37
|
-
*
|
|
36
|
+
* on `@ggui-ai/live-channel`'s `WSTransport` (post-B3b owner of the
|
|
37
|
+
* reconnect ladder). The router only watches status transitions; it
|
|
38
|
+
* doesn't own the reconnect schedule.
|
|
38
39
|
*
|
|
39
|
-
* **Idempotence** — re-subscribing the same `(
|
|
40
|
+
* **Idempotence** — re-subscribing the same `(sessionId, channelName)`
|
|
40
41
|
* pair is server-side idempotent (the server's `ChannelSubscriptionState`
|
|
41
42
|
* replaces in place). Clients can re-send on reconnect without bookkeeping
|
|
42
43
|
* a "did we already subscribe?" gate.
|
|
@@ -53,20 +54,21 @@
|
|
|
53
54
|
*/
|
|
54
55
|
import type { JsonObject, JsonValue, StreamSpec } from '@ggui-ai/protocol';
|
|
55
56
|
import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
|
|
57
|
+
import type { ChannelTransportFallbackEvent, ChannelTransportPickedEvent, ChannelTransportResubscribedEvent } from './observability.js';
|
|
56
58
|
import type { StreamBus } from './wire-config.js';
|
|
57
59
|
/**
|
|
58
60
|
* Default polling cadence for the iframe-polling transport (ms).
|
|
59
61
|
* Mirrors the server-side default on
|
|
60
62
|
* {@link DEFAULT_CHANNEL_POLL_DEFAULT_MS} in
|
|
61
|
-
* `packages/mcp-server/src/
|
|
63
|
+
* `packages/mcp-server/src/render-channel.ts`. Conservative: 10s
|
|
62
64
|
* matches a "data is fresh enough" bar without burning the parent
|
|
63
65
|
* host's `tools/call` quota.
|
|
64
66
|
*/
|
|
65
67
|
export declare const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10000;
|
|
66
68
|
/**
|
|
67
|
-
* Sender for outbound WS frames.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* Sender for outbound WS frames. Any structurally-fitting send fn
|
|
70
|
+
* works — post-B3b the registry wires the `@ggui-ai/live-channel`
|
|
71
|
+
* transport's send here.
|
|
70
72
|
*/
|
|
71
73
|
export type WsSender = (msg: WebSocketMessage) => void;
|
|
72
74
|
/**
|
|
@@ -83,9 +85,9 @@ export type ToolsCallInvoker = (args: {
|
|
|
83
85
|
* Options for {@link createChannelTransportRouter}.
|
|
84
86
|
*/
|
|
85
87
|
export interface ChannelTransportRouterOptions {
|
|
86
|
-
/**
|
|
87
|
-
readonly
|
|
88
|
-
/** App (tenant) id paired with
|
|
88
|
+
/** GguiSession id the router scopes channel_subscribe frames against. */
|
|
89
|
+
readonly sessionId: string;
|
|
90
|
+
/** App (tenant) id paired with sessionId on the subscribe frame. */
|
|
89
91
|
readonly appId: string;
|
|
90
92
|
/**
|
|
91
93
|
* Allowlist of `source.tool` names this server can subscribe-for
|
|
@@ -116,24 +118,12 @@ export interface ChannelTransportRouterOptions {
|
|
|
116
118
|
/**
|
|
117
119
|
* Observability events the router emits. Hosts that wire `onObserve`
|
|
118
120
|
* see these alongside the renderer's other observability events
|
|
119
|
-
* (`subscribe-failed`, `
|
|
120
|
-
*
|
|
121
|
+
* (`subscribe-failed`, `auth-required`, …). The arm interfaces live in
|
|
122
|
+
* `observability.ts` (they are members of the exported
|
|
123
|
+
* `ObservabilityEvent` union); this alias is the router-scoped subset,
|
|
124
|
+
* so the two surfaces cannot drift.
|
|
121
125
|
*/
|
|
122
|
-
export type ChannelTransportEvent =
|
|
123
|
-
readonly kind: 'channel-transport-picked';
|
|
124
|
-
readonly renderId: string;
|
|
125
|
-
readonly channelName: string;
|
|
126
|
-
readonly transport: 'ws' | 'poll';
|
|
127
|
-
} | {
|
|
128
|
-
readonly kind: 'channel-transport-fallback';
|
|
129
|
-
readonly renderId: string;
|
|
130
|
-
readonly channelName: string;
|
|
131
|
-
readonly reason: 'ws-disconnect' | 'channel-not-local';
|
|
132
|
-
} | {
|
|
133
|
-
readonly kind: 'channel-transport-resubscribed';
|
|
134
|
-
readonly renderId: string;
|
|
135
|
-
readonly channelName: string;
|
|
136
|
-
};
|
|
126
|
+
export type ChannelTransportEvent = ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent;
|
|
137
127
|
/**
|
|
138
128
|
* Router handle returned by {@link createChannelTransportRouter}.
|
|
139
129
|
*/
|
|
@@ -148,7 +138,7 @@ export interface ChannelTransportRouter {
|
|
|
148
138
|
* the source-fed subset.
|
|
149
139
|
*/
|
|
150
140
|
readonly applyRender: (render: {
|
|
151
|
-
readonly
|
|
141
|
+
readonly sessionId: string;
|
|
152
142
|
readonly streamSpec?: StreamSpec;
|
|
153
143
|
}) => void;
|
|
154
144
|
/**
|
|
@@ -175,7 +165,7 @@ export interface ChannelTransportRouter {
|
|
|
175
165
|
/**
|
|
176
166
|
* Factory.
|
|
177
167
|
*
|
|
178
|
-
* Per-channel transport routing — owns the `(
|
|
168
|
+
* Per-channel transport routing — owns the `(sessionId, channelName)`
|
|
179
169
|
* registry, the WS-vs-poll decision per channel, and the cross-state
|
|
180
170
|
* transitions (disconnect → poll fallback, reconnect → re-subscribe).
|
|
181
171
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Default polling cadence for the iframe-polling transport (ms).
|
|
3
3
|
* Mirrors the server-side default on
|
|
4
4
|
* {@link DEFAULT_CHANNEL_POLL_DEFAULT_MS} in
|
|
5
|
-
* `packages/mcp-server/src/
|
|
5
|
+
* `packages/mcp-server/src/render-channel.ts`. Conservative: 10s
|
|
6
6
|
* matches a "data is fresh enough" bar without burning the parent
|
|
7
7
|
* host's `tools/call` quota.
|
|
8
8
|
*/
|
|
@@ -10,13 +10,13 @@ export const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10_000;
|
|
|
10
10
|
/**
|
|
11
11
|
* Factory.
|
|
12
12
|
*
|
|
13
|
-
* Per-channel transport routing — owns the `(
|
|
13
|
+
* Per-channel transport routing — owns the `(sessionId, channelName)`
|
|
14
14
|
* registry, the WS-vs-poll decision per channel, and the cross-state
|
|
15
15
|
* transitions (disconnect → poll fallback, reconnect → re-subscribe).
|
|
16
16
|
*/
|
|
17
17
|
export function createChannelTransportRouter(opts) {
|
|
18
18
|
/**
|
|
19
|
-
* Registry keyed by `${
|
|
19
|
+
* Registry keyed by `${sessionId}:${channelName}`. The composite
|
|
20
20
|
* key matches the server-side `channelSubs` map shape exactly, so
|
|
21
21
|
* test snapshots line up.
|
|
22
22
|
*/
|
|
@@ -34,8 +34,8 @@ export function createChannelTransportRouter(opts) {
|
|
|
34
34
|
let wsStatus = 'connected';
|
|
35
35
|
let disposed = false;
|
|
36
36
|
const observe = opts.onObserve ?? (() => { });
|
|
37
|
-
function keyOf(
|
|
38
|
-
return `${
|
|
37
|
+
function keyOf(sessionId, channelName) {
|
|
38
|
+
return `${sessionId}:${channelName}`;
|
|
39
39
|
}
|
|
40
40
|
function stopPolling(state) {
|
|
41
41
|
if (state.pollTimer !== null) {
|
|
@@ -45,7 +45,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
45
45
|
}
|
|
46
46
|
function emitToBus(state, payload, complete) {
|
|
47
47
|
const envelope = {
|
|
48
|
-
|
|
48
|
+
sessionId: state.sessionId,
|
|
49
49
|
channel: state.channelName,
|
|
50
50
|
mode: state.mode,
|
|
51
51
|
payload,
|
|
@@ -90,14 +90,14 @@ export function createChannelTransportRouter(opts) {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Send a `channel_subscribe` WS frame. Server-side bookkeeping is
|
|
93
|
-
* idempotent on `(
|
|
93
|
+
* idempotent on `(sessionId, channelName)`, so re-sends on reconnect
|
|
94
94
|
* are safe.
|
|
95
95
|
*/
|
|
96
96
|
function sendSubscribe(state) {
|
|
97
97
|
opts.send({
|
|
98
98
|
type: 'channel_subscribe',
|
|
99
99
|
payload: {
|
|
100
|
-
|
|
100
|
+
sessionId: state.sessionId,
|
|
101
101
|
appId: opts.appId,
|
|
102
102
|
channelName: state.channelName,
|
|
103
103
|
...(state.args !== undefined ? { args: { ...state.args } } : {}),
|
|
@@ -114,7 +114,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
114
114
|
opts.send({
|
|
115
115
|
type: 'channel_unsubscribe',
|
|
116
116
|
payload: {
|
|
117
|
-
|
|
117
|
+
sessionId: state.sessionId,
|
|
118
118
|
appId: opts.appId,
|
|
119
119
|
channelName: state.channelName,
|
|
120
120
|
},
|
|
@@ -137,7 +137,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
137
137
|
sendSubscribe(state);
|
|
138
138
|
observe({
|
|
139
139
|
kind: 'channel-transport-picked',
|
|
140
|
-
|
|
140
|
+
sessionId: state.sessionId,
|
|
141
141
|
channelName: state.channelName,
|
|
142
142
|
transport: 'ws',
|
|
143
143
|
});
|
|
@@ -148,7 +148,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
148
148
|
startPolling(state);
|
|
149
149
|
observe({
|
|
150
150
|
kind: 'channel-transport-picked',
|
|
151
|
-
|
|
151
|
+
sessionId: state.sessionId,
|
|
152
152
|
channelName: state.channelName,
|
|
153
153
|
transport: 'poll',
|
|
154
154
|
});
|
|
@@ -157,7 +157,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
157
157
|
startPolling(state);
|
|
158
158
|
observe({
|
|
159
159
|
kind: 'channel-transport-picked',
|
|
160
|
-
|
|
160
|
+
sessionId: state.sessionId,
|
|
161
161
|
channelName: state.channelName,
|
|
162
162
|
transport: 'poll',
|
|
163
163
|
});
|
|
@@ -184,18 +184,18 @@ export function createChannelTransportRouter(opts) {
|
|
|
184
184
|
const channelArgs = source.args !== undefined && source.args !== null
|
|
185
185
|
? source.args
|
|
186
186
|
: undefined;
|
|
187
|
-
const k = keyOf(render.
|
|
187
|
+
const k = keyOf(render.sessionId, channelName);
|
|
188
188
|
seenKeys.add(k);
|
|
189
189
|
const existing = channels.get(k);
|
|
190
190
|
if (existing !== undefined) {
|
|
191
|
-
// Same (
|
|
191
|
+
// Same (sessionId, channelName) — leave the transport
|
|
192
192
|
// bookkeeping alone. Spec changes that flip preferWs
|
|
193
193
|
// mid-render are out of 1c scope (would require
|
|
194
194
|
// re-handshake).
|
|
195
195
|
continue;
|
|
196
196
|
}
|
|
197
197
|
const state = {
|
|
198
|
-
|
|
198
|
+
sessionId: render.sessionId,
|
|
199
199
|
channelName,
|
|
200
200
|
toolName,
|
|
201
201
|
...(channelArgs !== undefined ? { args: channelArgs } : {}),
|
|
@@ -213,7 +213,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
213
213
|
// mounts that haven't been re-applied yet. Close-render paths
|
|
214
214
|
// call `dispose()` for the wholesale teardown.
|
|
215
215
|
for (const [k, state] of channels) {
|
|
216
|
-
if (state.
|
|
216
|
+
if (state.sessionId === render.sessionId &&
|
|
217
217
|
!seenKeys.has(k)) {
|
|
218
218
|
stopPolling(state);
|
|
219
219
|
sendUnsubscribe(state);
|
|
@@ -226,7 +226,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
226
226
|
return false;
|
|
227
227
|
if (msg.type === 'channel_payload') {
|
|
228
228
|
const p = msg.payload;
|
|
229
|
-
const k = keyOf(p.
|
|
229
|
+
const k = keyOf(p.sessionId, p.channelName);
|
|
230
230
|
const state = channels.get(k);
|
|
231
231
|
if (state === undefined)
|
|
232
232
|
return false;
|
|
@@ -244,7 +244,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
244
244
|
if (msg.type === 'channel_error') {
|
|
245
245
|
const p = msg.payload;
|
|
246
246
|
// Locate by `channelName` only — the error payload doesn't
|
|
247
|
-
// carry `
|
|
247
|
+
// carry `sessionId`. We match by channel-name across active
|
|
248
248
|
// items and apply the permanent-fallback flag to the matching
|
|
249
249
|
// entries. Same channel name across renders is rare; the
|
|
250
250
|
// policy is conservative (fall back ALL matches rather than
|
|
@@ -254,12 +254,12 @@ export function createChannelTransportRouter(opts) {
|
|
|
254
254
|
continue;
|
|
255
255
|
if (p.code === 'CHANNEL_NOT_LOCAL' ||
|
|
256
256
|
p.code === 'CHANNEL_UNKNOWN' ||
|
|
257
|
-
p.code === '
|
|
257
|
+
p.code === 'SESSION_NOT_FOUND') {
|
|
258
258
|
state.permanentPollFallback = true;
|
|
259
259
|
startPolling(state);
|
|
260
260
|
observe({
|
|
261
261
|
kind: 'channel-transport-fallback',
|
|
262
|
-
|
|
262
|
+
sessionId: state.sessionId,
|
|
263
263
|
channelName: state.channelName,
|
|
264
264
|
reason: 'channel-not-local',
|
|
265
265
|
});
|
|
@@ -298,7 +298,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
298
298
|
startPolling(state);
|
|
299
299
|
observe({
|
|
300
300
|
kind: 'channel-transport-fallback',
|
|
301
|
-
|
|
301
|
+
sessionId: state.sessionId,
|
|
302
302
|
channelName: state.channelName,
|
|
303
303
|
reason: 'ws-disconnect',
|
|
304
304
|
});
|
|
@@ -309,8 +309,8 @@ export function createChannelTransportRouter(opts) {
|
|
|
309
309
|
if (status === 'connected' &&
|
|
310
310
|
(prevStatus === 'disconnected' || prevStatus === 'reconnecting')) {
|
|
311
311
|
// Reconnected. Re-send `channel_subscribe` for every
|
|
312
|
-
// WS-preferring channel. Server is idempotent on the
|
|
313
|
-
// render, channel) tuple, so duplicates are safe. We
|
|
312
|
+
// WS-preferring channel. Server is idempotent on the
|
|
313
|
+
// (render, channel) tuple, so duplicates are safe. We
|
|
314
314
|
// intentionally LEAVE the polling fallback running until the
|
|
315
315
|
// first `channel_payload` lands — bridges the gap where the
|
|
316
316
|
// server's first post-reconnect poll cycle hasn't fired yet.
|
|
@@ -322,7 +322,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
322
322
|
sendSubscribe(state);
|
|
323
323
|
observe({
|
|
324
324
|
kind: 'channel-transport-resubscribed',
|
|
325
|
-
|
|
325
|
+
sessionId: state.sessionId,
|
|
326
326
|
channelName: state.channelName,
|
|
327
327
|
});
|
|
328
328
|
}
|
package/dist/channels/data.d.ts
CHANGED
|
@@ -2,24 +2,17 @@
|
|
|
2
2
|
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
3
|
* against the active render's `streamSpec`, then fans the validated
|
|
4
4
|
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
|
-
* (matching `
|
|
5
|
+
* (matching `GguiRender.handleServerMessage`'s surfacing policy —
|
|
6
6
|
* the violation is already logged by the validator).
|
|
7
7
|
*
|
|
8
|
-
* Also fires `contract-error-emitted` on the optional observability
|
|
9
|
-
* sink when the envelope arrives on the reserved
|
|
10
|
-
* `CONTRACT_ERROR_CHANNEL`. Absorbed from `handleObservableMessage`
|
|
11
|
-
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
|
-
* sole dispatch surface for `data` frames.
|
|
13
|
-
*
|
|
14
8
|
* Post-stack-removal (2026-05-27): the active render is read through
|
|
15
|
-
* the caller-supplied `
|
|
9
|
+
* the caller-supplied `getCurrentGguiSession` thunk instead of via a
|
|
16
10
|
* `StackModel.snapshot()` walk — the iframe holds exactly one mounted
|
|
17
11
|
* render, so the lookup is direct.
|
|
18
12
|
*/
|
|
19
13
|
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
20
|
-
import {
|
|
21
|
-
import type {
|
|
22
|
-
import type { ObservabilityEmitter } from '../observability.js';
|
|
14
|
+
import type { GguiSession, StreamEnvelope } from '@ggui-ai/protocol';
|
|
15
|
+
import type { GguiSessionSeedInput } from '../types.js';
|
|
23
16
|
import { type RendererValidatorContext } from '../validation.js';
|
|
24
17
|
import type { StreamBus } from '../wire-config.js';
|
|
25
18
|
export interface DataHandlerDeps {
|
|
@@ -28,16 +21,8 @@ export interface DataHandlerDeps {
|
|
|
28
21
|
* has been mounted yet — data frames received pre-mount have no
|
|
29
22
|
* streamSpec to validate against and silently drop.
|
|
30
23
|
*/
|
|
31
|
-
readonly
|
|
24
|
+
readonly getCurrentGguiSession: () => GguiSession | GguiSessionSeedInput | null;
|
|
32
25
|
readonly streamBus: StreamBus;
|
|
33
26
|
readonly validatorCtx: RendererValidatorContext;
|
|
34
|
-
/**
|
|
35
|
-
* Optional observability sink. When present, every envelope on the
|
|
36
|
-
* reserved `_ggui:contract-error` channel fires a
|
|
37
|
-
* {@link ContractErrorEmittedEvent}. Absent = the observation skips
|
|
38
|
-
* silently (matches the pre-B3b posture when no `onObserve` is
|
|
39
|
-
* bound).
|
|
40
|
-
*/
|
|
41
|
-
readonly onObserve?: ObservabilityEmitter;
|
|
42
27
|
}
|
|
43
28
|
export declare function createDataHandler(deps: DataHandlerDeps): ChannelHandler<StreamEnvelope>;
|
package/dist/channels/data.js
CHANGED
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
3
|
* against the active render's `streamSpec`, then fans the validated
|
|
4
4
|
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
|
-
* (matching `
|
|
5
|
+
* (matching `GguiRender.handleServerMessage`'s surfacing policy —
|
|
6
6
|
* the violation is already logged by the validator).
|
|
7
7
|
*
|
|
8
|
-
* Also fires `contract-error-emitted` on the optional observability
|
|
9
|
-
* sink when the envelope arrives on the reserved
|
|
10
|
-
* `CONTRACT_ERROR_CHANNEL`. Absorbed from `handleObservableMessage`
|
|
11
|
-
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
|
-
* sole dispatch surface for `data` frames.
|
|
13
|
-
*
|
|
14
8
|
* Post-stack-removal (2026-05-27): the active render is read through
|
|
15
|
-
* the caller-supplied `
|
|
9
|
+
* the caller-supplied `getCurrentGguiSession` thunk instead of via a
|
|
16
10
|
* `StackModel.snapshot()` walk — the iframe holds exactly one mounted
|
|
17
11
|
* render, so the lookup is direct.
|
|
18
12
|
*/
|
|
19
|
-
import { CONTRACT_ERROR_CHANNEL, } from '@ggui-ai/protocol';
|
|
20
13
|
import { validateInboundStreamPayload, } from '../validation.js';
|
|
21
14
|
export function createDataHandler(deps) {
|
|
22
15
|
return {
|
|
@@ -27,19 +20,9 @@ export function createDataHandler(deps) {
|
|
|
27
20
|
typeof envelope.channel !== 'string') {
|
|
28
21
|
return;
|
|
29
22
|
}
|
|
30
|
-
// Observability-axis emission for the reserved contract-error
|
|
31
|
-
// channel. Fires BEFORE validation because the envelope shape
|
|
32
|
-
// is owned by the server's reserved validator
|
|
33
|
-
// (`BUILTIN_RESERVED_VALIDATORS`) — invalid payloads at this
|
|
34
|
-
// boundary are server-side bugs the host inspector should still
|
|
35
|
-
// surface, not silently drop.
|
|
36
|
-
if (envelope.channel === CONTRACT_ERROR_CHANNEL &&
|
|
37
|
-
deps.onObserve !== undefined) {
|
|
38
|
-
emitContractErrorFromDataFrame(envelope, deps.onObserve);
|
|
39
|
-
}
|
|
40
23
|
// Active render carries the streamSpec — mirrors
|
|
41
|
-
// `
|
|
42
|
-
const activeRender = deps.
|
|
24
|
+
// `GguiRender.handleServerMessage`.
|
|
25
|
+
const activeRender = deps.getCurrentGguiSession();
|
|
43
26
|
const streamSpec = activeRender !== null &&
|
|
44
27
|
activeRender.type !== 'mcpApps' &&
|
|
45
28
|
activeRender.type !== 'system'
|
|
@@ -52,34 +35,3 @@ export function createDataHandler(deps) {
|
|
|
52
35
|
},
|
|
53
36
|
};
|
|
54
37
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Narrow a contract-error envelope and emit
|
|
57
|
-
* {@link ContractErrorEmittedEvent}. Malformed envelopes silently
|
|
58
|
-
* skip (trust-boundary posture — host inspector gets a row only when
|
|
59
|
-
* the wire payload carries the required discriminator fields).
|
|
60
|
-
*/
|
|
61
|
-
function emitContractErrorFromDataFrame(envelope, emit) {
|
|
62
|
-
const payload = envelope.payload;
|
|
63
|
-
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const shaped = payload;
|
|
67
|
-
const code = shaped.error !== undefined &&
|
|
68
|
-
shaped.error !== null &&
|
|
69
|
-
typeof shaped.error === 'object' &&
|
|
70
|
-
typeof shaped.error.code === 'string'
|
|
71
|
-
? shaped.error.code
|
|
72
|
-
: undefined;
|
|
73
|
-
const toolName = typeof shaped.toolName === 'string' ? shaped.toolName : undefined;
|
|
74
|
-
if (code === undefined || toolName === undefined)
|
|
75
|
-
return;
|
|
76
|
-
const event = {
|
|
77
|
-
kind: 'contract-error-emitted',
|
|
78
|
-
code,
|
|
79
|
-
toolName,
|
|
80
|
-
...(typeof shaped.actionName === 'string'
|
|
81
|
-
? { actionName: shaped.actionName }
|
|
82
|
-
: {}),
|
|
83
|
-
};
|
|
84
|
-
emit(event);
|
|
85
|
-
}
|
package/dist/channels/index.d.ts
CHANGED
|
@@ -2,6 +2,5 @@ export { createPropsUpdateHandler, type PropsUpdateHandlerDeps, } from './props-
|
|
|
2
2
|
export { createDrainAckHandler, type DrainAckHandlerDeps, } from './drain-ack.js';
|
|
3
3
|
export { createRenderHandler, type RenderHandlerDeps } from './render-frame.js';
|
|
4
4
|
export { createDataHandler, type DataHandlerDeps } from './data.js';
|
|
5
|
-
export { createFeedbackHandler } from './feedback.js';
|
|
6
5
|
export { createChannelPayloadHandler, createChannelErrorHandler, type ChannelRouterHandlerDeps, } from './channel-payload.js';
|
|
7
6
|
export { createSystemHandler, type SystemHandlerDeps } from './system.js';
|
package/dist/channels/index.js
CHANGED
|
@@ -2,6 +2,5 @@ export { createPropsUpdateHandler, } from './props-update.js';
|
|
|
2
2
|
export { createDrainAckHandler, } from './drain-ack.js';
|
|
3
3
|
export { createRenderHandler } from './render-frame.js';
|
|
4
4
|
export { createDataHandler } from './data.js';
|
|
5
|
-
export { createFeedbackHandler } from './feedback.js';
|
|
6
5
|
export { createChannelPayloadHandler, createChannelErrorHandler, } from './channel-payload.js';
|
|
7
6
|
export { createSystemHandler } from './system.js';
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Post-stack-removal (2026-05-27): each iframe holds exactly one
|
|
7
7
|
* mounted render. The handler reads the current render via
|
|
8
|
-
* `
|
|
8
|
+
* `getCurrentGguiSession()`, validates the inbound props against its
|
|
9
9
|
* cached `propsSpec`, and re-applies the patched render through
|
|
10
10
|
* `applyRender` — the same callback the render handler uses, so the
|
|
11
11
|
* React update surface is unified.
|
|
12
12
|
*
|
|
13
13
|
* Skips when:
|
|
14
|
-
* - `
|
|
14
|
+
* - `sessionId` is empty or not a string.
|
|
15
15
|
* - `props` is null / not an object (defensive — server can't emit
|
|
16
16
|
* this shape, but the dispatcher routes the frame on type alone).
|
|
17
|
-
* - No render is currently mounted (`
|
|
18
|
-
* - The current render's id doesn't match `payload.
|
|
17
|
+
* - No render is currently mounted (`getCurrentGguiSession` returns null).
|
|
18
|
+
* - The current render's id doesn't match `payload.sessionId` —
|
|
19
19
|
* the server may have raced ahead of an in-flight render swap.
|
|
20
20
|
* - The current render is `mcpApps` / `system` (no `propsSpec`;
|
|
21
21
|
* server should never emit `props_update` for these).
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
*
|
|
24
24
|
* R6 (2026-05-26) retired the per-handler polling descriptor. Polling
|
|
25
25
|
* is now registry-level — the iframe-runtime composes the
|
|
26
|
-
* `/api/
|
|
26
|
+
* `/api/sessions/:id/state?wsToken=<token>` URL and a snapshot-parsing
|
|
27
27
|
* function once at bind time (see `runtime.ts`); a single fetch per
|
|
28
28
|
* tick projects the slice envelope into per-handler frames the
|
|
29
29
|
* `PollingTransport` dispatches.
|
|
30
30
|
*/
|
|
31
31
|
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
32
|
-
import type { PropsUpdatePayload,
|
|
33
|
-
import type {
|
|
32
|
+
import type { PropsUpdatePayload, GguiSession } from '@ggui-ai/protocol';
|
|
33
|
+
import type { GguiSessionSeedInput } from '../types.js';
|
|
34
34
|
export interface PropsUpdateHandlerDeps {
|
|
35
35
|
/**
|
|
36
36
|
* Read the currently-mounted render. Returns `null` when no
|
|
@@ -39,11 +39,11 @@ export interface PropsUpdateHandlerDeps {
|
|
|
39
39
|
* `props_update` before the first render frame has no React tree to
|
|
40
40
|
* patch.
|
|
41
41
|
*/
|
|
42
|
-
readonly
|
|
42
|
+
readonly getCurrentGguiSession: () => GguiSession | GguiSessionSeedInput | null;
|
|
43
43
|
/**
|
|
44
44
|
* Re-apply the patched render to the single mount slot. Shared with
|
|
45
45
|
* the render-frame handler so React updates flow through one path.
|
|
46
46
|
*/
|
|
47
|
-
readonly applyRender: (render:
|
|
47
|
+
readonly applyRender: (render: GguiSession | GguiSessionSeedInput) => Promise<void>;
|
|
48
48
|
}
|
|
49
49
|
export declare function createPropsUpdateHandler(deps: PropsUpdateHandlerDeps): ChannelHandler<PropsUpdatePayload>;
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Post-stack-removal (2026-05-27): each iframe holds exactly one
|
|
7
7
|
* mounted render. The handler reads the current render via
|
|
8
|
-
* `
|
|
8
|
+
* `getCurrentGguiSession()`, validates the inbound props against its
|
|
9
9
|
* cached `propsSpec`, and re-applies the patched render through
|
|
10
10
|
* `applyRender` — the same callback the render handler uses, so the
|
|
11
11
|
* React update surface is unified.
|
|
12
12
|
*
|
|
13
13
|
* Skips when:
|
|
14
|
-
* - `
|
|
14
|
+
* - `sessionId` is empty or not a string.
|
|
15
15
|
* - `props` is null / not an object (defensive — server can't emit
|
|
16
16
|
* this shape, but the dispatcher routes the frame on type alone).
|
|
17
|
-
* - No render is currently mounted (`
|
|
18
|
-
* - The current render's id doesn't match `payload.
|
|
17
|
+
* - No render is currently mounted (`getCurrentGguiSession` returns null).
|
|
18
|
+
* - The current render's id doesn't match `payload.sessionId` —
|
|
19
19
|
* the server may have raced ahead of an in-flight render swap.
|
|
20
20
|
* - The current render is `mcpApps` / `system` (no `propsSpec`;
|
|
21
21
|
* server should never emit `props_update` for these).
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
* R6 (2026-05-26) retired the per-handler polling descriptor. Polling
|
|
25
25
|
* is now registry-level — the iframe-runtime composes the
|
|
26
|
-
* `/api/
|
|
26
|
+
* `/api/sessions/:id/state?wsToken=<token>` URL and a snapshot-parsing
|
|
27
27
|
* function once at bind time (see `runtime.ts`); a single fetch per
|
|
28
28
|
* tick projects the slice envelope into per-handler frames the
|
|
29
29
|
* `PollingTransport` dispatches.
|
|
@@ -33,15 +33,15 @@ export function createPropsUpdateHandler(deps) {
|
|
|
33
33
|
return {
|
|
34
34
|
type: 'props_update',
|
|
35
35
|
onMessage: async (payload) => {
|
|
36
|
-
const {
|
|
37
|
-
if (typeof
|
|
36
|
+
const { sessionId, props } = payload;
|
|
37
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0)
|
|
38
38
|
return;
|
|
39
39
|
if (props === null || typeof props !== 'object')
|
|
40
40
|
return;
|
|
41
|
-
const current = deps.
|
|
41
|
+
const current = deps.getCurrentGguiSession();
|
|
42
42
|
if (current === null)
|
|
43
43
|
return;
|
|
44
|
-
if (current.id !==
|
|
44
|
+
if (current.id !== sessionId)
|
|
45
45
|
return;
|
|
46
46
|
if (current.type === 'mcpApps' || current.type === 'system')
|
|
47
47
|
return;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* exactly one mounted render for its lifetime. The handler routes the
|
|
9
9
|
* inbound `render` to a caller-supplied `applyRender` callback that
|
|
10
10
|
* either mounts (first frame) or re-applies (subsequent frames to the
|
|
11
|
-
* same id). Frames addressed to a different `
|
|
11
|
+
* same id). Frames addressed to a different `sessionId` are out-of-spec
|
|
12
12
|
* and drop with a console warning — the host spawns a fresh iframe
|
|
13
13
|
* per unique render id.
|
|
14
14
|
*
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* happens. The boot-without-renderer path remains testable.
|
|
19
19
|
*/
|
|
20
20
|
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
21
|
-
import type { RenderPayload,
|
|
21
|
+
import type { RenderPayload, GguiSession } from '@ggui-ai/protocol';
|
|
22
22
|
import type { ChannelTransportRouter } from '../channel-transport.js';
|
|
23
23
|
import { type StatusRefs } from '../status-dom.js';
|
|
24
24
|
export interface RenderHandlerDeps {
|
|
@@ -29,19 +29,19 @@ export interface RenderHandlerDeps {
|
|
|
29
29
|
*/
|
|
30
30
|
readonly statusRefs: StatusRefs;
|
|
31
31
|
/**
|
|
32
|
-
* Pin — when set, render frames with a different `
|
|
32
|
+
* Pin — when set, render frames with a different `sessionId`
|
|
33
33
|
* are dropped with a console warning. Set by the renderer hook to
|
|
34
34
|
* the render id the iframe was bootstrapped against (every
|
|
35
35
|
* post-displayMode iframe is pinned to a single render).
|
|
36
36
|
*/
|
|
37
|
-
readonly
|
|
37
|
+
readonly pinnedSessionId?: string;
|
|
38
38
|
/**
|
|
39
39
|
* Apply the inbound render to the single mount slot. Absent in
|
|
40
40
|
* placeholder mode (no React mount). When present, the handler
|
|
41
41
|
* `await`s the apply before activating the per-channel transport
|
|
42
42
|
* so the router targets the render React already mounted.
|
|
43
43
|
*/
|
|
44
|
-
readonly applyRender?: (render:
|
|
44
|
+
readonly applyRender?: (render: GguiSession) => Promise<void>;
|
|
45
45
|
/**
|
|
46
46
|
* Renderer-mode hook — returns the per-channel transport router so
|
|
47
47
|
* the render handler can activate `source.tool` channel subscriptions
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* exactly one mounted render for its lifetime. The handler routes the
|
|
9
9
|
* inbound `render` to a caller-supplied `applyRender` callback that
|
|
10
10
|
* either mounts (first frame) or re-applies (subsequent frames to the
|
|
11
|
-
* same id). Frames addressed to a different `
|
|
11
|
+
* same id). Frames addressed to a different `sessionId` are out-of-spec
|
|
12
12
|
* and drop with a console warning — the host spawns a fresh iframe
|
|
13
13
|
* per unique render id.
|
|
14
14
|
*
|
|
@@ -22,15 +22,15 @@ export function createRenderHandler(deps) {
|
|
|
22
22
|
return {
|
|
23
23
|
type: 'render',
|
|
24
24
|
onMessage: async (payload) => {
|
|
25
|
-
const render = payload.
|
|
26
|
-
if (deps.
|
|
25
|
+
const render = payload.session;
|
|
26
|
+
if (deps.pinnedSessionId !== undefined && render.id !== deps.pinnedSessionId) {
|
|
27
27
|
// Out-of-spec: each iframe is pinned to exactly one render id
|
|
28
28
|
// post-displayMode-divergence; the host spawns a fresh iframe
|
|
29
29
|
// per render. A frame for some other id means the host wired
|
|
30
30
|
// the wrong render or the server is broadcasting cross-render
|
|
31
31
|
// frames.
|
|
32
32
|
// eslint-disable-next-line no-console
|
|
33
|
-
console.warn(`[ggui:render] ignoring render for ${render.id} — iframe pinned to ${deps.
|
|
33
|
+
console.warn(`[ggui:render] ignoring render for ${render.id} — iframe pinned to ${deps.pinnedSessionId}`);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
if (deps.applyRender !== undefined) {
|
|
@@ -43,7 +43,7 @@ export function createRenderHandler(deps) {
|
|
|
43
43
|
if (render.type === 'mcpApps' || render.type === 'system')
|
|
44
44
|
return;
|
|
45
45
|
channelTransport.applyRender({
|
|
46
|
-
|
|
46
|
+
sessionId: render.id,
|
|
47
47
|
...(render.streamSpec !== undefined
|
|
48
48
|
? { streamSpec: render.streamSpec }
|
|
49
49
|
: {}),
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { CompiledContractValidators } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
15
15
|
import type { ValidateFunction } from '@ggui-ai/protocol';
|
|
16
16
|
/**
|
|
17
|
-
* Precompiled validators for the active
|
|
17
|
+
* Precompiled validators for the active render's contract, keyed to
|
|
18
18
|
* match the four runtime-validated surfaces. `props` is a single
|
|
19
19
|
* validator (the synthesized props wrapper); the other three are keyed
|
|
20
20
|
* by action name / channel name / slot name.
|
|
@@ -49,7 +49,7 @@ export declare function loadCompiledValidators(meta: CompiledContractValidators
|
|
|
49
49
|
* Response shape: an ES module whose `default` export is a
|
|
50
50
|
* {@link CompiledContractValidators} object (produced server-side by
|
|
51
51
|
* `bundleCompiledValidatorsAsModule`). `Cache-Control: immutable` so
|
|
52
|
-
* repeat
|
|
52
|
+
* repeat renders with the same contract hit browser cache with no
|
|
53
53
|
* round-trip.
|
|
54
54
|
*/
|
|
55
55
|
export declare function loadCompiledValidatorsFromUrl(url: string | undefined, warn?: (message: string, detail: unknown) => void): Promise<CompiledValidatorSet>;
|
|
@@ -83,7 +83,7 @@ export async function loadCompiledValidators(meta, warn) {
|
|
|
83
83
|
* Response shape: an ES module whose `default` export is a
|
|
84
84
|
* {@link CompiledContractValidators} object (produced server-side by
|
|
85
85
|
* `bundleCompiledValidatorsAsModule`). `Cache-Control: immutable` so
|
|
86
|
-
* repeat
|
|
86
|
+
* repeat renders with the same contract hit browser cache with no
|
|
87
87
|
* round-trip.
|
|
88
88
|
*/
|
|
89
89
|
export async function loadCompiledValidatorsFromUrl(url, warn) {
|