@ggui-ai/iframe-runtime 0.13.0 → 0.15.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/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './m
37
37
  export type { RendererBootFailureReason, RendererBootFailedMessage, } from './runtime.js';
38
38
  export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
39
39
  export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
40
- export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayIncapabilityEvent, UnknownObservabilityEvent, } from './observability.js';
41
- export { postObservabilityToParent } from './observability.js';
40
+ export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
41
+ export { isObservabilityMessage, isRelayDeadTapEvent, isRelayIncapabilityEvent, postObservabilityToParent, } from './observability.js';
42
42
  export type { LifecycleEmitter } from './lifecycle.js';
43
43
  export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
2
2
  export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
3
- export { postObservabilityToParent } from './observability.js';
3
+ export { isObservabilityMessage, isRelayDeadTapEvent, isRelayIncapabilityEvent, postObservabilityToParent, } from './observability.js';
4
4
  export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
@@ -50,7 +50,7 @@
50
50
  * At least one mode MUST be present. Half-live (wsUrl without wsToken)
51
51
  * is structurally rejected by the combiner.
52
52
  */
53
- import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol';
53
+ import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol/wire';
54
54
  import { parseMcpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
55
55
  /**
56
56
  * Type guard — true iff `value` is a non-null, non-array plain object.
@@ -33,7 +33,7 @@ import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
33
33
  *
34
34
  * @public
35
35
  */
36
- export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | UnknownObservabilityEvent;
36
+ export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | UnknownObservabilityEvent;
37
37
  /**
38
38
  * Fired when the protocol-version handshake rejects the connection.
39
39
  * Parallel to `UpgradeRequiredError` — the host can choose to render
@@ -178,36 +178,122 @@ export interface UiFeedbackEvent {
178
178
  /** Tool that produced the render (e.g. `ggui_render`). */
179
179
  readonly toolName?: string;
180
180
  }
181
+ /**
182
+ * The evidence that latched a relay dead zone:
183
+ * - `'confirmed-refusal'` — the relay answered a declared refusal code
184
+ * (helper-minted; outranks any positive advertisement) (ggui#599
185
+ * cycle-2);
186
+ * - `'advert-silent'` — the host never advertised `serverTools` and
187
+ * the attempt failed relay-shaped;
188
+ * - `'boot-failed'` — the mandatory `ui/initialize` handshake did not
189
+ * complete (refused, timed out, or the transport failed), so
190
+ * nothing can ever be delivered in this document (ggui#830). Not a
191
+ * probe the runtime minted: the outcome of the one request every
192
+ * session begins with — the strongest confirmed outcome there is.
193
+ * It clears the only way a handshake failure can: the next boot's
194
+ * close edge.
195
+ *
196
+ * The union is closed per renderer version; nothing in a host branches
197
+ * on the trigger (the first-party host passes it through untouched) —
198
+ * the edge is the fact, the trigger is why. A host that does branch on
199
+ * it should treat a value it does not know as "latched, reason
200
+ * unknown", never as "not latched".
201
+ *
202
+ * @public
203
+ */
204
+ export type RelayLatchTrigger = 'confirmed-refusal' | 'advert-silent' | 'boot-failed';
181
205
  /**
182
206
  * Fired at the two transition edges of the renderer's
183
207
  * relay-incapability latch — the runtime's confirmed determination
184
208
  * that the host cannot relay `tools/call` to the MCP server:
185
209
  *
186
- * - `'latched'` — a real user gesture just failed relay-shaped (no
187
- * well-formed result envelope came back at all) on a host whose
188
- * captured capability handshake never advertised `serverTools`.
210
+ * - `'latched'` — a real user gesture just failed relay-shaped on a
211
+ * host whose captured capability handshake never advertised
212
+ * `serverTools`, or the relay answered a declared refusal code, or
213
+ * the host refused `ui/initialize` itself (`'boot-failed'`,
214
+ * ggui#830 — no gesture is needed to know nothing can be delivered).
189
215
  * The runtime now treats relay as confirmed-unavailable: it shows
190
- * one persistent explanation instead of a per-gesture error toast,
191
- * and channel polls fail fast without a transport round-trip.
216
+ * one persistent explanation, arms the dead-zone cue for every
217
+ * later gesture, and channel polls fail fast without a transport
218
+ * round-trip.
192
219
  * - `'cleared'` — a later well-formed result envelope arrived
193
220
  * (`ok:true` and `ok:false` alike — either proves the host relayed
194
- * the call there and back), so the determination no longer holds;
195
- * per-gesture feedback and channel transport attempts resume.
221
+ * the call there and back), so the determination no longer holds.
222
+ * Carries the dead zone's summary: how long it stood and how many
223
+ * gestures were attempted inside it.
196
224
  *
197
- * Emitted once per edge, never per channel poll tick: repeated failing
198
- * gestures while latched emit nothing further, and the router's
199
- * fail-fast ticks emit nothing at all — the edges carry the full
200
- * information.
225
+ * Emitted once per edge, never per channel poll tick. Gestures
226
+ * attempted while latched are NOT silent on this stream: each one is a
227
+ * {@link RelayDeadTapEvent} (ggui#670 Phase 3) — one per real user
228
+ * gesture, never per tick — so a host can count dead taps and measure
229
+ * dead-tap-to-recovery without watching the render. A boot that finds
230
+ * a latch standing from a prior mount closes it with a `'cleared'`
231
+ * edge before the new session starts.
201
232
  *
202
- * Always emitted via the postMessage-to-parent default — the emission
203
- * sites live in module-level gesture-dispatch code outside the boot
204
- * graph, so they never flow through an injected `onObserve` sink.
233
+ * Always emitted via the postMessage-to-parent default, by rule — the
234
+ * gesture-dispatch emission sites run outside any given boot call, and
235
+ * the boot-graph site (the `boot-failed` edge, ggui#830) follows the
236
+ * same rule — so these edges never flow through an injected
237
+ * `onObserve` sink and a host reads every edge the same way.
205
238
  *
206
239
  * @public
207
240
  */
208
- export interface RelayIncapabilityEvent {
241
+ export type RelayIncapabilityEvent = {
242
+ readonly kind: 'relay-incapability';
243
+ readonly state: 'latched';
244
+ readonly trigger: RelayLatchTrigger;
245
+ /** GguiSession id of the render, when the latching gesture carried one. */
246
+ readonly sessionId?: string;
247
+ /** App id of the render, when the latching gesture carried one. */
248
+ readonly appId?: string;
249
+ } | {
209
250
  readonly kind: 'relay-incapability';
210
- readonly state: 'latched' | 'cleared';
251
+ readonly state: 'cleared';
252
+ /** How long the dead zone stood, in milliseconds. */
253
+ readonly latchedForMs: number;
254
+ /**
255
+ * Gestures attempted inside the zone — equals the last
256
+ * {@link RelayDeadTapEvent} `ordinal` this edge closes (0 when
257
+ * none was attempted).
258
+ */
259
+ readonly deadTaps: number;
260
+ readonly sessionId?: string;
261
+ readonly appId?: string;
262
+ };
263
+ /**
264
+ * A user gesture attempted while the relay-incapability latch stands
265
+ * (ggui#670 Phase 3). The runtime never suppresses the attempt — the
266
+ * attempt is the self-heal sensor (ggui#443) — and presents it in the
267
+ * document (pulse, spoken cue, fallback toast); this event makes the
268
+ * attempt COUNTABLE by the host.
269
+ *
270
+ * Contract: exactly one per gesture whose UNDELIVERED outcome
271
+ * (relay-shaped failure or a declared refusal) lands while the latch
272
+ * stands — an in-flight gesture that raced the latching one counts;
273
+ * never emitted off-latch, and never from the channel
274
+ * router's fail-fast ticks. Counted at the outcome, not the tap: the
275
+ * gesture that latched the zone is the `'latched'` edge and the gesture
276
+ * that heals it is the `'cleared'` edge — neither is a dead tap.
277
+ * `ordinal` counts from 1 within one dead zone; the `'cleared'` edge
278
+ * reports the total as `deadTaps`. `latchAgeMs` is measured when the
279
+ * outcome lands. Hosts MUST tolerate the event being absent (an older
280
+ * renderer) — absence means no attempt was observed, not that none
281
+ * happened.
282
+ *
283
+ * @public
284
+ */
285
+ export interface RelayDeadTapEvent {
286
+ readonly kind: 'relay-dead-tap';
287
+ /** The gesture's intent — the action name the component dispatched. */
288
+ readonly intent: string;
289
+ /** Milliseconds since the latch set. */
290
+ readonly latchAgeMs: number;
291
+ /** 1-based position of this attempt within the current dead zone. */
292
+ readonly ordinal: number;
293
+ /** The evidence that latched the zone this attempt landed in. */
294
+ readonly trigger: RelayLatchTrigger;
295
+ readonly sessionId?: string;
296
+ readonly appId?: string;
211
297
  }
212
298
  /**
213
299
  * Catch-all branch for event kinds the host's typings don't recognize.
@@ -245,6 +331,52 @@ export interface ObservabilityMessage {
245
331
  * @public
246
332
  */
247
333
  export type ObservabilityEmitter = (event: ObservabilityEvent) => void;
334
+ /**
335
+ * Type guard for the `ggui:observe` envelope as it arrives on a host's
336
+ * `message` listener — narrows `event.data` to {@link ObservabilityMessage}.
337
+ * Read fields through the per-kind guards, never through
338
+ * `switch (event.kind)`: the union's open tail (`kind: string & {}`)
339
+ * keeps TypeScript from using `kind` as a discriminant, so a `case`
340
+ * body sees the whole union. Unknown kinds stay in that tail: hosts
341
+ * MUST ignore them, never throw.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * import {
346
+ * isObservabilityMessage,
347
+ * isRelayDeadTapEvent,
348
+ * isRelayIncapabilityEvent,
349
+ * } from '@ggui-ai/iframe-runtime/observability';
350
+ *
351
+ * window.addEventListener('message', ({ data, source }) => {
352
+ * if (source !== iframe.contentWindow || !isObservabilityMessage(data)) return;
353
+ * const { event } = data;
354
+ * if (isRelayDeadTapEvent(event)) count(event.intent, event.latchAgeMs);
355
+ * else if (isRelayIncapabilityEvent(event) && event.state === 'cleared') recovered(event.deadTaps);
356
+ * // any other kind: ignore
357
+ * });
358
+ * ```
359
+ *
360
+ * @public
361
+ */
362
+ export declare function isObservabilityMessage(value: unknown): value is ObservabilityMessage;
363
+ /**
364
+ * Narrows to {@link RelayDeadTapEvent}. The union's open tail
365
+ * (`kind: string & {}`) keeps TypeScript from using `kind` as a
366
+ * discriminant, so `switch (event.kind)` never narrows across
367
+ * {@link ObservabilityEvent} — hosts read fields through the per-kind
368
+ * guards (first-consumer finding, ggui#670 Phase 3).
369
+ *
370
+ * @public
371
+ */
372
+ export declare function isRelayDeadTapEvent(event: ObservabilityEvent): event is RelayDeadTapEvent;
373
+ /**
374
+ * Narrows to {@link RelayIncapabilityEvent}; inside it `state`
375
+ * discriminates the `'latched'` / `'cleared'` edges normally.
376
+ *
377
+ * @public
378
+ */
379
+ export declare function isRelayIncapabilityEvent(event: ObservabilityEvent): event is RelayIncapabilityEvent;
248
380
  /**
249
381
  * Default emitter — posts an {@link ObservabilityMessage} to
250
382
  * `window.parent`. Mirrors `postBootFailure` / `postRendererReady`
@@ -1,4 +1,63 @@
1
1
  import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
2
+ /**
3
+ * Type guard for the `ggui:observe` envelope as it arrives on a host's
4
+ * `message` listener — narrows `event.data` to {@link ObservabilityMessage}.
5
+ * Read fields through the per-kind guards, never through
6
+ * `switch (event.kind)`: the union's open tail (`kind: string & {}`)
7
+ * keeps TypeScript from using `kind` as a discriminant, so a `case`
8
+ * body sees the whole union. Unknown kinds stay in that tail: hosts
9
+ * MUST ignore them, never throw.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import {
14
+ * isObservabilityMessage,
15
+ * isRelayDeadTapEvent,
16
+ * isRelayIncapabilityEvent,
17
+ * } from '@ggui-ai/iframe-runtime/observability';
18
+ *
19
+ * window.addEventListener('message', ({ data, source }) => {
20
+ * if (source !== iframe.contentWindow || !isObservabilityMessage(data)) return;
21
+ * const { event } = data;
22
+ * if (isRelayDeadTapEvent(event)) count(event.intent, event.latchAgeMs);
23
+ * else if (isRelayIncapabilityEvent(event) && event.state === 'cleared') recovered(event.deadTaps);
24
+ * // any other kind: ignore
25
+ * });
26
+ * ```
27
+ *
28
+ * @public
29
+ */
30
+ export function isObservabilityMessage(value) {
31
+ if (typeof value !== 'object' || value === null)
32
+ return false;
33
+ const probe = value;
34
+ if (probe.type !== MCP_APP_OBSERVE_TYPE)
35
+ return false;
36
+ if (typeof probe.event !== 'object' || probe.event === null)
37
+ return false;
38
+ return typeof probe.event.kind === 'string';
39
+ }
40
+ /**
41
+ * Narrows to {@link RelayDeadTapEvent}. The union's open tail
42
+ * (`kind: string & {}`) keeps TypeScript from using `kind` as a
43
+ * discriminant, so `switch (event.kind)` never narrows across
44
+ * {@link ObservabilityEvent} — hosts read fields through the per-kind
45
+ * guards (first-consumer finding, ggui#670 Phase 3).
46
+ *
47
+ * @public
48
+ */
49
+ export function isRelayDeadTapEvent(event) {
50
+ return event.kind === 'relay-dead-tap';
51
+ }
52
+ /**
53
+ * Narrows to {@link RelayIncapabilityEvent}; inside it `state`
54
+ * discriminates the `'latched'` / `'cleared'` edges normally.
55
+ *
56
+ * @public
57
+ */
58
+ export function isRelayIncapabilityEvent(event) {
59
+ return event.kind === 'relay-incapability';
60
+ }
2
61
  /**
3
62
  * Default emitter — posts an {@link ObservabilityMessage} to
4
63
  * `window.parent`. Mirrors `postBootFailure` / `postRendererReady`
@@ -31,7 +31,7 @@
31
31
  * while suspended, matching host-SDK behavior.
32
32
  */
33
33
  import React, { type ReactNode } from 'react';
34
- import type { StreamEnvelope } from '@ggui-ai/protocol';
34
+ import type { StreamEnvelope } from '@ggui-ai/protocol/wire';
35
35
  export interface ProvisionalMount {
36
36
  /**
37
37
  * Feed a stream envelope. Ignores non-PREVIEW_CHANNEL envelopes
@@ -30,6 +30,7 @@
30
30
  * TypeScript config doesn't need a JSX runtime plumbing.
31
31
  */
32
32
  import React, { type ReactNode } from 'react';
33
+ import type { AppTheme } from '@ggui-ai/protocol/wire';
33
34
  export interface ReactRootMountOptions {
34
35
  /**
35
36
  * GguiSession whose componentCode + props drive the mount. Only
@@ -98,11 +99,7 @@ export interface ReactRootMountOptions {
98
99
  * painted ladder to its registration record); the renderer
99
100
  * carries it but exposes no observability surface for it today.
100
101
  */
101
- readonly base?: {
102
- readonly documentHash: string;
103
- readonly light: Readonly<Record<string, string>>;
104
- readonly dark: Readonly<Record<string, string>>;
105
- };
102
+ readonly base?: NonNullable<AppTheme['base']>;
106
103
  };
107
104
  /**
108
105
  * Host-announced palette, already translated onto `--ggui-*` tokens
@@ -329,7 +329,15 @@ export async function mountReactRoot(container, opts) {
329
329
  // ladder gets — base inherits, derived gradient/effect tokens
330
330
  // (color-mix fallback split), box-sizing, font-inherit — from
331
331
  // ONE shared builder, so the two paths cannot drift.
332
- assembleDeliveredThemeCss(scopeClass, opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light)
332
+ assembleDeliveredThemeCss(scopeClass, opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light, {
333
+ // ggui#613 residual 2 — the two former v1 deltas ride:
334
+ // the active mode's keyframes block + the frameless
335
+ // silhouette suppression, both same-rule as compiled.
336
+ keyframes: opts.themeMode === 'dark'
337
+ ? deliveredBase.keyframes?.dark
338
+ : deliveredBase.keyframes?.light,
339
+ frameless: deliveredBase.frameless,
340
+ })
333
341
  : opts.themeId
334
342
  ? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
335
343
  : getScopedCssTokens(scopeClass, opts.themeMode);
@@ -367,9 +375,17 @@ export async function mountReactRoot(container, opts) {
367
375
  // `ThemeProvider` so a Studio/Portal context with both runtime + RP
368
376
  // active doesn't double-stack.
369
377
  if (typeof document !== 'undefined') {
370
- let rootCss = opts.themeId
371
- ? getThemeCss(opts.themeId, opts.themeMode)
372
- : getCssTokens(opts.themeMode);
378
+ // ggui#613 residual 5: under a DELIVERED base the body-chrome
379
+ // ladder is the delivered set too — mirroring the scoped ternary
380
+ // above — so the standalone viewer's body chrome and its tree
381
+ // content resolve the same values. Pre-fix this started from the
382
+ // compiled/default ladder regardless, and body chrome painted the
383
+ // default brand under a registered theme.
384
+ let rootCss = deliveredBase !== undefined
385
+ ? `:root{${toCssDecls(opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light)}}`
386
+ : opts.themeId
387
+ ? getThemeCss(opts.themeId, opts.themeMode)
388
+ : getCssTokens(opts.themeMode);
373
389
  if (opts.hostPalette) {
374
390
  // BODY-CHROME ONLY, same as the appTheme `:root` append below —
375
391
  // and beneath it, mirroring the scoped-block order (#573:
@@ -1,4 +1,4 @@
1
- import type { AckPayload } from '@ggui-ai/protocol';
1
+ import type { AckPayload } from '@ggui-ai/protocol/wire';
2
2
  import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
3
3
  import type { McpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
4
4
  import type { AnyTransportHandle, ChannelLogger, ChannelRegistry, RegistryPollingOptions, RegistrySseOptions, WsTransportHandle } from '@ggui-ai/live-channel';
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { GguiSession } from '@ggui-ai/protocol';
2
+ import type { AppTheme, GguiSession } from '@ggui-ai/protocol/wire';
3
3
  import type { GguiSessionSeedInput } from './types.js';
4
4
  import { type WireConfig } from '@ggui-ai/wire';
5
5
  import type { StreamBus } from './wire-config.js';
@@ -59,11 +59,7 @@ export interface RenderItemOptions {
59
59
  readonly appTheme?: {
60
60
  readonly mode: 'light' | 'dark';
61
61
  readonly cssVariables: Record<string, string>;
62
- readonly base?: {
63
- readonly documentHash: string;
64
- readonly light: Readonly<Record<string, string>>;
65
- readonly dark: Readonly<Record<string, string>>;
66
- };
62
+ readonly base?: NonNullable<AppTheme['base']>;
67
63
  };
68
64
  /**
69
65
  * Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
@@ -1,4 +1,4 @@
1
- import { RUNTIME_TELEMETRY_MAX_EVENTS } from '@ggui-ai/protocol';
1
+ import { RUNTIME_TELEMETRY_MAX_EVENTS } from '@ggui-ai/protocol/wire';
2
2
  const FIRST_FLUSH_DELAY_MS = 4_000;
3
3
  const FLUSH_THROTTLE_MS = 8_000;
4
4
  const MAX_FLUSHES_PER_SESSION = 12;
package/dist/runtime.d.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  * wrapper, which routes it to the host's `onError` callback.
26
26
  */
27
27
  import './zod-jitless.js';
28
- import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-ai/protocol';
28
+ import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-ai/protocol/wire';
29
29
  import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
30
30
  import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type McpAppRendererReadyMessage } from '@ggui-ai/protocol/integrations/mcp-apps';
31
31
  import type { GguiSessionSeedInput } from './types.js';
@@ -56,6 +56,15 @@ import { type LifecycleEmitter } from './lifecycle.js';
56
56
  */
57
57
  type DrainAckListener = (payload: DrainAckPayload) => boolean | void;
58
58
  export declare function subscribeDrainAck(listener: DrainAckListener): () => void;
59
+ /**
60
+ * The `drain_ack` ingress — fans a frame out to subscribers until one
61
+ * claims it.
62
+ *
63
+ * @internal — exported for unit tests: a late ack for an earlier
64
+ * delivered gesture is the path that retires the standing relay notice
65
+ * without any user click (ggui#670 Phase 3, truth-2).
66
+ */
67
+ export declare function dispatchDrainAck(payload: DrainAckPayload): void;
59
68
  export type ConnectAppResult = {
60
69
  readonly ok: true;
61
70
  } | {
@@ -270,10 +279,12 @@ export interface BootSequenceOptions {
270
279
  * entirely (matches the ProtocolError posture; the `<McpAppIframe>`
271
280
  * host wrapper decides whether to bind this via its `onObserve`
272
281
  * prop). Exception: `relay-incapability` events do NOT flow through
273
- * this seam — they originate in module-level gesture-dispatch code
274
- * outside the boot graph (it runs independently of any given boot
275
- * call) and always ride the postMessage-to-parent default, whether
276
- * or not this option is bound.
282
+ * this seam — by rule they always ride the postMessage-to-parent
283
+ * default, whether or not this option is bound: most emission sites
284
+ * are module-level gesture-dispatch code that runs independently of
285
+ * any given boot call, and the one inside the boot graph (the
286
+ * `boot-failed` edge, ggui#830) follows the same rule so a host reads
287
+ * every edge the same way.
277
288
  */
278
289
  readonly onObserve?: ObservabilityEmitter;
279
290
  /**
@@ -660,6 +671,16 @@ interface ToastLiveRegions {
660
671
  * @internal — exported for unit tests.
661
672
  */
662
673
  export declare function ensureToastAnnouncer(doc: Document): ToastLiveRegions | null;
674
+ /**
675
+ * Boot-time reset of the relay latch (ggui#670 Phase 3). A latch
676
+ * standing from a prior mount in a re-booted document is closed through
677
+ * the ONE writer — the host gets its `cleared` edge with the zone's
678
+ * summary — and the prior mount's standing notice is removed rather
679
+ * than left describing a session that no longer exists.
680
+ *
681
+ * @internal — exported for unit tests.
682
+ */
683
+ export declare function resetRelayLatchForBoot(): void;
663
684
  /** @internal — exported for unit tests to reset module state. */
664
685
  export declare function __resetRelayNoticeForTest(): void;
665
686
  /**
@@ -685,19 +706,22 @@ export declare function dispatchSubmitAction(args: {
685
706
  /**
686
707
  * Channel-transport router's `tools/call` invoker (ggui#440) —
687
708
  * iframe-polling transport, `tools/call` against the parent MCP host
688
- * via `app.callServerTool` (spec-canonical) when the App handle is
689
- * set; falls back to raw postMessage pre-handshake or in tests.
709
+ * via `app.callServerTool` (spec-canonical); with no App bound the
710
+ * call answers a code-less error envelope — never a raw postMessage.
690
711
  * Direct call (no LLM consent loop). Returns the tool's
691
712
  * structuredContent (or `content[0]` if that's where the payload
692
713
  * landed) as a `JsonValue`. On RPC error we throw — the router
693
714
  * catches and silently retries on the next tick.
694
715
  *
695
- * Once a REAL gesture has confirmed this host can't relay (the latch
696
- * above — set only after {@link dispatchSubmitAction} actually tried
697
- * and failed on a host with no `serverTools` advertised, with
698
- * capabilities already captured, AND the failure was relay-shaped —
699
- * not a well-formed `{ok:false}` result; see `isRelayShapedFailure`),
700
- * every subsequent poll will fail identically.
716
+ * Once this document has confirmed it cannot relay (the latch above —
717
+ * set through the ONE writer on exactly three paths: a REAL gesture
718
+ * that failed relay-shaped on a host that never advertised
719
+ * `serverTools`, with capabilities already captured (`advert-silent`);
720
+ * a declared refusal code answered under a positive advertisement
721
+ * (`confirmed-refusal`); or the host's own failure of the mandatory
722
+ * `ui/initialize` handshake, before any gesture (`boot-failed`,
723
+ * ggui#830) — never a well-formed `{ok:false}` result; see
724
+ * `isRelayShapedFailure`), every subsequent poll will fail identically.
701
725
  *
702
726
  * This guard does NOT stop the router's poll loop, and nothing here
703
727
  * should ever be written so that it could (ggui#443). It does two
@@ -723,10 +747,13 @@ export declare function dispatchSubmitAction(args: {
723
747
  * (ggui's own embed host pre-Task-2 is exactly this case) — silently
724
748
  * killing working channel polls forever. That is "absence of a
725
749
  * capability blocks an attempt", which the fail-safe constraint
726
- * forbids. The latch implies `!hostCanRelayToolCalls()` (it only
727
- * ever sets inside that guard), so testing it alone is sufficient —
728
- * a host that never fails a real gesture never latches, and its
729
- * channels keep working.
750
+ * forbids. The latch is set only by a CONFIRMED OUTCOME — a failed
751
+ * real gesture (the advert-silent leg, or a confirmed refusal under a
752
+ * positive advertisement, ggui#599 cycle-2) or a failed handshake
753
+ * (ggui#830), never by an advertisement's absence — so testing the
754
+ * latch alone is sufficient: a host that answers its handshake and
755
+ * never fails a real gesture never latches, and its channels keep
756
+ * working.
730
757
  *
731
758
  * Extracted to a named export (rather than an inline closure inside
732
759
  * `bootProduction`) so unit tests can drive it directly through the