Skip to content
Public preview · the console is open

What's new

read as .md

The GGUI protocol is a pre-1.0 draft (currently draft-2026-09-10) — it can still change. This page tracks releases and protocol-affecting changes as they ship. For the semver rules behind what counts as breaking, see Version policy.

  • Changed, in @ggui-ai/ui-gen (#1711): the visual judge is no longer asked state.feedback on a capture with no post-action state. Its own text makes it not evaluated then, and a judged frame is one capture taken before any action. The row now reads n/a (“not evaluated: no post-action capture”) and stays in the block’s verdicts, so it is counted as considered. With a post-action capture (postActionCapture), the judge is asked as for any row. REQUIRES_POST_ACTION_CAPTURE names the rows this applies to. Report-only; no score changes.
  • Changed, in @ggui-ai/design (#1567): low-emphasis text reads as text in every shipped theme. The subtle tone now resolves to the theme’s muted ink (onSunken), like muted; ten components that painted hint text with neutral-500 (Autocomplete, Breadcrumb, FormField, ChatWindow, CommandPalette, CommentThread, DataTable, FileUploader, Footer, NotificationCenter) use the tone, so a theme shows one muted grey; NotificationCenter’s unread row sits on the sunken ground and its timestamps use the tone; and the default theme’s muted ink is darker in light (#596273) and lighter in dark (#adb9ca). Every shipped theme’s muted ink now clears 4.5:1 on both the container and the sunken ground. In gguiDark, hint text goes from 4.36:1 to 5.80:1 on sunken; gguiLight is unchanged. The tone keeps its name, so code that asks for subtle needs no change.
  • Fixed, in @ggui-ai/mcp-server-core and @ggui-ai/mcp-server-handlers (#1339): the host-session pair a ggui_render request carries on _meta["ai.ggui/host-session"] is now set once by the first commit that carries it — fill-absent-never-overwrite, the rule the row’s subject already follows — so a session row born before the render (a hosted server creates it at the handshake’s provisional preview) still takes the pair; the in-memory and sqlite stores set it on their replace path, the conformance kit pins it on every store, and a later commit with a different pair is kept as it was and named once in the server’s log (host_session_conflict, ids only). A slice on the ggui_handshake request is ignored, and the extension doc now says so.

2026-10-02 — v0.26.0: the server refuses a spent one-shot, the SMTP sender moves to nodemailer 10 — upgrade if you embed it — and unused exports go

Section titled “2026-10-02 — v0.26.0: the server refuses a spent one-shot, the SMTP sender moves to nodemailer 10 — upgrade if you embed it — and unused exports go”
  • Removed exports (a pre-1.0 minor, VERSION-POLICY §3.7). Before 1.0, a minor release may remove an export without a deprecation window. A caret range such as ^0.25.0 never picks up a new minor, so nothing changes for you until you raise the range. Every export 0.25.0 had that 0.26.0 does not:

    • @ggui-ai/negotiator: hashContract, buildVariant;
    • @ggui-ai/mcp-server-core: mintSessionToken, DEFAULT_SESSION_TOKEN_TTL_SEC, nextSpentOneShotsRecord, and the variant selector (selectVariantWithLlm, encodeSelectedReason, preFilterCandidates, computeVariantSelectionCacheKey, VariantSelectionContext, VariantSelectionDecision, VariantSelectionCache, VariantSelectionCacheEntry, VariantSelectionPickFn, VariantSelectionResult, VariantSelectorWithLlmOptions, DEFAULT_VARIANT_SELECTION_CONFIDENCE_THRESHOLD, DEFAULT_VARIANT_SELECTION_CACHE_TTL_SEC, DEFAULT_VARIANT_SELECTION_SHORTLIST_SIZE, BlueprintSelector, BlueprintSelectorContext, createDeterministicBlueprintSelector, and on /in-memory InMemoryVariantSelectionCache, InMemoryVariantSelectionCacheOptions);
    • @ggui-ai/mcp-server-handlers: GenerationDeps.resolveLlmCaller, HandshakeNegotiator.selectVariant, REFRESH_WINDOW_CLOSED in GguiRefreshWsTokenOutput’s code, and validatorScore on ggui_ops_generate_blueprint’s output;
    • @ggui-ai/mcp-server: GguiSessionChannelBootstrap.issueSessionToken, createGguiServer’s blueprintSelector option and GguiServer.blueprintSelector;
    • @ggui-ai/protocol: validatorScore on opsGenerateBlueprintOutputSchema.

    Each is described, with its replacement where it has one, in its own line below. AckPayload.sessionToken and TokenKind’s 'session' stay one more release.

  • Added, in @ggui-ai/iframe-runtime (#1679): a card whose component throws past the error boundary’s retry is now reported to the server — one ggui_runtime_report_render_failure per session, carrying the phase (mount or update), the thrown value’s class name and the boundary’s catch count, with the view proof on the call. Nothing the visitor sees changes, and a report that is refused or lost is never resent. The tool itself has been served since #1609; this is the first runtime that sends it. In @ggui-ai/protocol, renderFailureErrorName now recognises an Error from another realm by its brand, so a card module evaluated in the document’s realm reports its class name rather than Error; the report’s vocabulary gains its own pure-const subpath, @ggui-ai/protocol/render-failure, for runtimes that must not bundle the schema modules (the root re-exports the same names).

  • Fixed, in @ggui-ai/mcp-server-handlers and @ggui-ai/mcp-server-core (#1424): the server now refuses a second gesture on a spent one-shot action. ggui_runtime_submit_action claims the spend before it appends the gesture, and a later dispatch of the same declared oneShot on the same card under a different actionId answers {ok:false, code:"CONTRACT_VIOLATION"} with one violation at actionSpec.<name>.oneShot and reaches no agent. Before, the spend was recorded after the append and never changed the answer, so a client that was not the ggui runtime could fire a one-shot twice. The same actionId is still a retry; a holder that claimed and never delivered is taken over by the next gesture, which is delivered once. The session store’s recordSpentOneShot now answers a SpentOneShotClaim (recorded, already-spent with the holder and its delivery mark, superseded, released). A store that still answers nothing keeps the earlier behaviour, and the conformance kit names it as a skip. The read view spentOneShots is unchanged.

  • Changed (a pre-1.0 minor, VERSION-POLICY §3.7), in @ggui-ai/mcp-server-core (#1424): nextSpentOneShotsRecord is removed. Its replacement is claimSpentOneShot over a SpentOneShotsLedger, which claims the spend instead of recording it after the fact. A custom session store that built its record with the old helper moves to the claim; a store that never recorded spends (its recordSpentOneShot is optional) changes nothing.

  • Removed (a pre-1.0 minor, VERSION-POLICY §3.7), in @ggui-ai/negotiator (#1334): hashContract and buildVariant. This ships in 0.26.0, a minor, and must not ride a 0.25.x patch: a caret range on 0.25.x never picks it up. Neither had a caller. hashContract hashed the intent alone and was never the cache key: every contractHash on the wire is blueprintKey(contract). Replacements: for hashContract, blueprintKey from @ggui-ai/protocol/blueprint-key; for buildVariant, none (variantKey, from the same subpath, is the key of a blueprint’s variance and is not the same thing). invokeRequestSchema (@ggui-ai/protocol) stays and now names who speaks it: the host helpers’ useInvoke sends that body, and its test parses the body with the schema.

  • Fixed, in @ggui-ai/mcp-server-handlers and @ggui-ai/protocol (#1339): ggui_render now captures the host’s conversation-grouping slice, _meta["ai.ggui/host-session"] (hostName + hostSessionId), on the session it creates, so ggui_list_sessions filtered by that pair returns the session. The slice, the store field and the list filter were documented and shipped, but no render passed the slice to the store, so the filter matched nothing. The render request is the one that must carry the slice (the handshake creates no session). Each field is now bounded at 256 characters (MCP_APP_HOST_SESSION_FIELD_MAX_LENGTH, new, from @ggui-ai/protocol/integrations/mcp-apps): parseMcpAppAiGguiHostSessionMeta returns MALFORMED_HOST_SESSION for a longer field, and the server treats a malformed slice as absent, with one host_session_malformed log line; the render still succeeds. A deployment whose session store persists hostSession starts writing it on new rows.

  • Fixed, in @ggui-ai/mcp-server-handlers (#1428): when ggui_handshake proposes a saved interface found by similarity, it now also compares the members of each action both contracts declare that change what happens on a gesture: oneShot, confirm and nextStep. If the saved interface’s action lacks one your draft declares (or names a different nextStep), the proposal carries a COVERAGE_GAP warning at actionSpec.<name>.<member>, so you can accept it knowing the card will not behave that way, or override. Before, only action names were compared: a saved card without oneShot was proposed for a draft that declared it, with no warning. Reuse stays the default; a proposal with such a gap now ranks below one without any gap. An exact-key match is unaffected (those members are part of the key).

  • Fixed, in @ggui-ai/mcp-server-handlers (#1429): a render whose generator returns no contract now commits the contract the handshake agreed, whole. Before, only propsSpec was kept: the session committed no actionSpec, streamSpec or contextSpec, so a declared oneShot was not recorded as spent and ggui_update / ggui_amend validated against nothing. A returned contract is still taken as given: it is complete, not a patch over the agreed one, and UIGenerationResponse.contract (@ggui-ai/protocol) now says so. When a returned contract leaves out a spec or an entry the agreed contract declares, the server logs one generator_contract_narrower line naming what was dropped. The generator shipped in @ggui-ai/ui-gen returns the agreed contract, so its renders do not change.

  • Fixed, in @ggui-ai/mcp-server (#1562): nodemailer moves from ^9.1.1 to ^10.0.13. That clears two high-severity advisories (GHSA-prgh-xp8r-p3m5 and GHSA-v53p-9fqp-m79j: address parsing that a crafted address can make quadratic, a denial of service) and three moderate ones (GHSA-g57g-f23g-4646, GHSA-8vvx-rff5-p5rq, GHSA-6vj9-mwq6-2f5v). The built-in email login bounds the address it sends to, but SmtpEmailSender is exported, and an embedder passing its own addresses was exposed. nodemailer 10 needs Node.js 20 or later (this package already requires 22) and ships its own types, so @types/nodemailer is no longer a dependency.

  • Changed, in @ggui-ai/ui-gen (#1652): the runtime-render check looks for each action by walking the card’s screens in a mount of its own, pressing each control once per screen. A wizard’s last step, an Edit button that becomes Save, and a form behind a button are reached, so their actions are verified instead of reported as not dispatched. Screens are told apart by their controls (labels and disabled state), never by typed values or generated names, so Back and Next cannot loop. Nothing bounds it while it is on the first screen with controls left to press there; once a press leaves the first screen, it stops at 60 presses, 3 s per action, or the check’s time budget. An action it cannot dispatch says how far the walk got (RenderCheckIssue.diagnostics.walk), not the first button on the page. The action check no longer presses the copy of the card that prop coverage, prop sensitivity and stream re-render read (it is still primed as before, and only the duplicate-label check still clicks there). Prop sensitivity therefore compares two unpressed renders, and can report a baked prop it used to miss on a card the old check pressed. Prop coverage counts a prop shown on any screen the walk reached.

  • Added, in @ggui-ai/mcp-server-handlers (#1553): renderReadVerdict (from @ggui-ai/mcp-server-handlers/renders) returns the render read gate’s answer with the rung that decided it; renderReadAllowed is its boolean. The two doors that use the gate, the ui:// render resource read and ggui_runtime_refresh_ws_token, now log one structured line, render_read_app_trust_over_subject {door, appId, source}, when they admit a caller with no end-user identity to a session that has one. That is the one case where this gate and the agent doors’ visibility check disagree, logged so its frequency can be measured before the two are unified. No access changes.

  • Fixed, in @ggui-ai/negotiator (#1430): a repaired handshake draft keeps every member the agent declared on each entry it keeps under the same key, on every spec, not only on actions: a stream’s mode, replay and complete, a prop’s default, a context slot’s debounceMs, a tool’s serverInfo, example and toolInfo.description, and the rest. A tool the repair dropped from the agent’s agentCapabilities.tools is put back whole and named REPAIR_ENTRY_RESTORED (severity warn), so an action’s valid nextStep survives the repair. A draft entry the repair renamed, moved or removed is named once, REPAIR_ENTRY_DROPPED, on every spec. The repair’s reasoning counts drops and restored tools apart.

  • Fixed, in @ggui-ai/protocol (#1518): when the self-contained shell (gguiShellHtml) cannot load its runtime bundle, it no longer leaves its loading mark animating. After it posts ggui:bootstrap-failed / BUNDLE_FETCH_FAILED, it removes the mark and paints a failure line, “This view could not load”, with a Retry that reloads the frame. That is the wording the thin shell already uses. A host that ignores the envelope, such as a third-party MCP Apps host reading the shell through resources/read, now shows the failure instead of a spinner. Nothing is painted while the unhashed twin is still being tried.

  • Removed, in @ggui-ai/mcp-server-handlers (#1510, a wire removal under VERSION-POLICY §3.6): ggui_runtime_refresh_ws_token’s advertised output schema no longer names REFRESH_WINDOW_CLOSED in its code enum, and the exported GguiRefreshWsTokenOutput no longer types it. No server has sent it since v0.25.0 made the refresh an authorized re-mint (#1496). It stayed declared for that one release, so a client that cached the older schema accepted every answer, and such a client still does.

  • Added, in @ggui-ai/ui-gen (#1640): judgedFrameTokens (from @ggui-ai/ui-gen/evaluation) sums a judged frame’s or a visual evaluation’s spend over every leg the judge reports: the scoring calls’ inputTokens / outputTokens and the report-only criteria call’s criteriaTokens. A cost total built on it counts the criteria call, and a token leg added to the judge’s result fails the package’s build until the sum counts it. Additive; the judge’s results are unchanged.

  • Added, in @ggui-ai/protocol (#1381): a @ggui-ai/protocol/runtime-telemetry subpath with the runtime telemetry vocabulary. RUNTIME_TELEMETRY_KINDS lists every kind a card’s runtime emits, which of the two batches it rides, and the one detail shape it admits, so the emitter and a host that admits only a closed set import the same names. CHANNEL_LOG_EVENTS and ChannelLogEvent move here from @ggui-ai/live-channel, which re-exports them unchanged. The subpath has no runtime imports. @ggui-ai/live-channel now depends on @ggui-ai/protocol, where it had no dependencies before: installing it alone now installs protocol and its dependencies, while a bundle takes only this subpath’s constants. The server still logs a kind it does not name and never refuses one.

  • GguiServer.close() in @ggui-ai/mcp-server is bounded (#1631). It stops accepting connections and closes idle ones at once, then lets active ones finish for up to graceMs (default DEFAULT_CLOSE_GRACE_MS, 5 s, now exported) before it ends every connection still open. So a client that never finishes its request, or never reads a response, can no longer hold a shutdown open. Pass close({ graceMs: Infinity }) for the previous unbounded wait.

  • Added, in @ggui-ai/ui-gen (#1663): a report-only alignment measurement. Before each screenshot, the visual evaluator reads the card’s left edges from the DOM (EDGE_BLOCKS_EXPRESSION, typed by parseEdgeProbe) and applies the alignment rule to them (judgeEdges), reporting every offset in px. In a criteria block, the space.edge row gains instrument: { verdict, evidence } beside the judge’s verdict, which it does not change; a stored frame with no page to read says so. CriterionVerdict.instrument is optional, and nothing changes for a block without that row.

  • Added, in @ggui-ai/ui-gen (#1687): the visual judge’s record says which model answered and what its criteria call returned. Each canvas’s judge record gains models: { requested, served }; served lists the names the provider gave for the model that answered (Anthropic’s model, Google’s modelVersion), and is empty when none did. A criteria block gains call: whether the answer held a criteria array, how many entries it had, how many were kept, how many were dropped and why, how many named no row of the bank, and the call’s stop reason. A row the judge left unanswered still reads n/a; the block now says whether the call came back empty, cut, or with entries the parser dropped. Both fields are optional and report-only.

  • @ggui-ai/ui-gen’s runtime-render check and render smoke test no longer mistake a component’s own console output for their verdict. Each worker now writes its verdict on one marked line, and the host reads that line. Before, a generated component that logged even one line while it was checked turned its check into an unverified “verdict was not valid JSON”.

  • Changed, in @ggui-ai/mcp-server (#1397): a ggui_consume result that drains a gesture no longer leads with a plain-text sentence telling the agent to repaint and then call ggui_consume again. The nextStep hint to ggui_amend stays in the structured result. Measured on two models: the sentence led one of them to answer an informational tap with a silent re-poll instead of a reply, and removing it kept every repaint.

  • Fixed, in @ggui-ai/protocol (#1637): the JSON Schema served on tools/list for ggui_handshake, ggui_render and ggui_runtime_sync_context is now one Google’s Gemini API accepts as function parameters. The JSON-value definition’s object arm no longer refers back to itself (its members are open, additionalProperties: {}, and checked in code). Before, Gemini refused the whole tool list. The values accepted are unchanged; an invalid value nested inside an object is now reported at the member that holds it.

  • Added, in @ggui-ai/protocol (#1175): the theme GET names a stored theme it cannot read. When the stored document fails the carry shape, the response is theme: null beside uninterpretable: { issueCount, issues: [{ path, code }] }, so it no longer reads like “no theme”. Codes come from the protocol’s own closed THEME_ISSUE_CODES, mapped by describeUninterpretableTheme. No stored value is carried, and a bare theme: null still means no theme. A reader that ignores the new member sees exactly the previous response.

  • A copied blueprint row now says it is a copy (#1570). ggui_ops_register_blueprint takes an optional clonedFrom, the id of the row whose bytes it registers, and so does createRegisterGeneratedBlueprint (@ggui-ai/mcp-server-handlers, the in-process entry), which parses the same input. The durable row carries it, so ggui_ops_list_blueprints lists it. Over MCP a server from before this strips the member, so a copy sent to it lands unmarked; check its tools/list before relying on the mark. An empty id, the row’s own id, and an id that is not a row of the registering app are refused before anything is persisted (ClonedFromRefusedError), with the same answer for another app’s row as for a missing one. Nothing that serves reads the field.

  • A card’s runtime telemetry travels in two batches (#1383), never mixed in one ggui_runtime_telemetry call. The health batch carries which rung the card booted on (boot.path, boot.static_only_no_bridge), its connection status (status.<connecting|connected|disconnected|reconnecting>), subscribe.resolved, doorbell.ring and the live channel’s transport events (the channel_* names). Each health kind admits one detail shape (none, the card’s own render id, or a JSON object of booleans), and a detail outside it is left off the event. The diagnostic batch carries the gesture trail and the ladder’s finer story, and no longer carries free text: gesture.dispatch keeps {toolName}, gesture.result keeps {ok} and the JSON-RPC error code (never the message), gesture.dropped_superseded carries no detail, and subscribe.resolved is {hasAck} only. The health batch flushes first, and the two batches have separate per-session flush caps (8 and 4). @ggui-ai/iframe-runtime exports the one table of kinds as RUNTIME_TELEMETRY_KINDS (with RuntimeTelemetryKind), and @ggui-ai/live-channel exports its transports’ log events as CHANNEL_LOG_EVENTS (with ChannelLogEvent, which ChannelLogger’s methods now take).

  • Added, in @ggui-ai/protocol and @ggui-ai/mcp-server-handlers (#1609): ggui_runtime_report_render_failure, an app-only runtime tool a card uses to say its render failed. It carries the phase (mount or update), the thrown value’s class name (a code identifier, or "Error") and the catch count, never a message or a stack. The server records a report only for the session the call’s view proof binds, answers ok whatever the verdict, and never treats a report as an agent turn. @ggui-ai/mcp-server names each proven report (render_failed), and an unproven one without its session (render_failure_unproven). A deployment keeps a mark through the handler’s recordRenderFailure. No card sends it yet (SPEC §4.9). New exports: reportRenderFailureInputShape / Schema, reportRenderFailureOutputSchema, GguiReportRenderFailureInput / Output, RENDER_FAILURE_PHASES, RENDER_FAILURE_ERROR_NAME_PATTERN, RENDER_FAILURE_MAX_CATCHES, RENDER_FAILURE_MAX_ID_LENGTH (sessionId and appId are 1 to 256 characters), renderFailureErrorName, and createGguiReportRenderFailureHandler. A card sends at most one report per session. VIEW_PROOF_V1_BOUND_ARGS gains the tool’s row.

  • Added, in @ggui-ai/mcp-server-core (#1568): RenderIdentityRecord.requestedVariantKey, the variantKey() of the variance the agent itself named for a render (its render override.variance, else its handshake draft’s), beside the variantKey that was served. The two differ exactly when something other than the agent chose the served variance. ggui_render writes it on every commit. A record written before it existed reads without it, and absent means unknown, never the default variant. The render-identity refresh (ggui_update, ggui_runtime_sync_context) now carries every member it does not refresh from the stored record, so it keeps this member and any added later. runRenderIdentityStoreConformance asserts the round-trip and the absence.

  • Added, in @ggui-ai/ui-gen (#1398): the runtime-render check reads whether a control whose click dispatched an action visibly shows it is working while the action is pending (disabled, busy, new text, or replaced), and reports it as EvalResult.runtimeProbe.pendingAffordance: { dispatched, visible, missing, gone }. It runs after every other check, so it cannot change what they find. A form is read through its control that names the action. A pressed control stays that action’s control while it is on the page, even when it relabels itself. One that left the page, or now names another action, is gone, never credited to another control. It is report-only: it adds no issue and doesn’t change the probe’s verdict. It is absent when no action walk ran, or when too little of the check’s time was left.

  • Added, in @ggui-ai/mcp-server-handlers (#1568): a deployment can reserve rows from the semantic (similar-interface) reuse tier. MatchBlueprintOptions.excludeFromSemantic names rows by semanticExclusionKey(contractKey, variantKey). A named row is never offered to the reuse judge or returned as a candidate, and it stays reachable by its exact key. HandshakeDecisionAdapter.semanticExclusion answers once per request: { kind: 'rows', keys }, or { kind: 'all', reason } when the deployment cannot say which rows are reserved. 'all', or a hook that fails, turns the semantic tier off for that request rather than risk offering a reserved row. MatchBlueprintOptions.disableSemanticCause names why the tier is off, so a trace never reports the exact-only policy for an exclusion outage. Nothing changes for a deployment that sets neither.

  • A theme’s motion.reduce: 'ignore' is accepted and treated as 'respect' (#1326). A card never overrides its viewer’s reduced-motion request: the reduced-motion rule applies under prefers-reduced-motion: reduce for every theme, because the preference is the viewer’s own accessibility setting. That was already the behaviour; the theme types and the document door now say so, and a stored document that says 'ignore' still parses.

  • The host-helper conformance kit grades the two host obligations of the view-origin proof (#1415, SPEC §4.7), when the host supplies its rules. V1-view-material-withheld feeds the host’s model-context rules a render result and a render read body whose _meta["ai.ggui/render"] slice carries a marker in every value. None may reach the model, raw, percent-encoded or inside a base64 run, the result’s own marker must, and the host’s model-facing resource list, when it has one, must withhold ui://ggui/render/*. L1-view-locator-binding feeds the host’s relay decision a view’s own and another session’s reads, in every locator form, and every session-naming call a view may make. Both are optional (modelContext, viewBinding options) and grade the rule, not a running host.

  • On hosted GGUI, TypeSafe AI’s Jev model now decides whether a render reuses a cached interface, and which one (#1235). GGUI’s LLM judge decides when Jev does not answer in time, returns an error, or is not confident. Jev is shown the same fields the judge is, except the similarity scores, and what a render costs does not change. See Trust & security.

  • The conformance kit’s n1-compat catalog grades the view-origin proof across releases (#1415), on a new view-proof wire. A later proof version must still fit the relay door shape and read version_unknown, never malformed; a v1 proof whose root carries a claim a later release adds must still parse.

  • The conformance kit grades SHOULDs (#1526). A fixture may declare "level": "should"; an unmet SHOULD is a warning, never a failure: it lands in the new ConformanceResult.warned bucket with the same evidence a failure carries, streams as WARN through the new ConformanceReporter.onFixtureWarn, and is printed by the new formatWarnings. The CLI counts a warned fixture as executed and never changes its exit code for one. The first SHOULD case, fresh-subscribe-replays-reserved-preview (SPEC §12.2.1), expects a fresh subscribe to replay a known-reserved channel’s retained envelope after the ack, at a seq at or below its streamSeq, graded by the new replayAfterAck option on a stream-update expectation. Changed: ConformanceResult gains the required warned field, so code that builds a ConformanceResult by hand adds warned: []; the runner always sets it.

  • @ggui-ai/mcp-server issues view keys (#1415). The render slice’s new viewKey rides the ggui_render and ggui_update results and the data-plane resources/read of a render locator. It is minted with the slice’s wsToken, which now carries the signing secret’s key id and the door (kid, src). The control plane, isolated services, an anonymous caller, ggui_list_sessions, /state, the refresh and the console inspector never get one, and their tokens carry neither stamp; a read door that withholds one logs view_key_not_issued with why, as does a mint whose token is too long to carry a key. Under the development allow-all adapter every caller holds one identity and is keyed. A card from this release signs its runtime calls with it, and the server’s gate measures them. createGguiServer gains viewProof.keyedSince, from which a call without a valid proof reads current rather than legacy on its line. A runtime from before this release ignores the field.

  • GET /api/sessions/:id/state logs state_chain_expired {sessionId, rootAgeSec} for each renewal it refuses because the chain expired: the presented token is past its exp, or its chain has no second left (#1540). Only a token the server signed is counted. A rootAgeSec at or past the refresh window is a chain that reached its bound, and a smaller one is a token that went unrenewed. The 410 is unchanged.

  • GET /api/sessions/:id/events logs render_channel_polled_first {sessionId, appId, kind} on its first successful read per server process, session and kind (#1614), so a view on the HTTP polling rung leaves a mount mark tagged with its app, as render_channel_subscribed does on the WebSocket and SSE streams. kind is cold-mount for the boot fetch (sinceSequence=0&limit=1) and poll otherwise. The line comes only after every gate passes, never on a refusal, and never carries the query string. The server remembers the last 10,000 session-and-kind pairs, so a pair read again after that many newer ones logs again. Responses are unchanged.

  • The live channel carries the stream counter’s epoch (#1531). @ggui-ai/mcp-server stamps streamEpoch on every stream envelope and on the subscribe ack (from a buffer that has epochs; the in-memory one does). A subscriber whose session’s counter restarts while it stays connected now receives the new generation’s frames instead of losing every one whose seq is below its cursor, and the server logs stream_epoch_changed when that happens; a late frame of the epoch a subscriber left is dropped, so it never moves the subscriber back. A resume whose fromEpoch (WebSocket payload or ?fromEpoch= on the SSE stream) is not the session’s current epoch, or whose fromSeq is past anything the counter has assigned, replays everything retained and sets replayTruncated. A buffer without epochs behaves as before.

  • Added, in @ggui-ai/protocol/integrations/mcp-apps (#1415): McpAppAiGguiRenderMeta.viewKey, the view key a view proves its app-only runtime calls with, and parseMcpAppAiGguiRenderMeta keeps it only beside the wsUrl / wsToken pair it is rooted in and only in its 43-character base64url shape. No server issues one yet.

  • A card captures its render slice’s view key root (#1415, the view-origin proof; nothing signs with it yet). That root is P, the payload segment of the slice’s wsToken, and the slice’s viewKey. McpAppAiGguiMetaParseResult (@ggui-ai/iframe-runtime) carries it as viewRoot on the ok and EXPIRED_BOOTSTRAP arms, beside the meta and never inside it, captured before an expired credential leaves the meta. ViewRoot is exported. The card adopts a root only when P fits a proof and decodes to the slice’s own session. For that session it moves only to a later iat, and a later slice without a key never replaces it. Added, in @ggui-ai/protocol/integrations/mcp-apps: decodeViewRootClaims, which decodes a root to its claims, unauthenticated, and names why a string is not one (malformed, wrong_kind).

  • Once a card’s boot slice is resolved, the runtime removes the inline copies of its envelope that the shell left behind (#1415): globalThis.__GGUI_META__, the pre-load __GGUI_PENDING_TOOL_RESULTS__ buffer, and the self-contained shell’s envelope <script> element, whose text outlives the global. Nothing in the runtime reads them after boot. Code that read __GGUI_META__ after the runtime booted no longer finds it. This is hygiene, not a boundary: card code runs in the same realm and can still ask the host to read its own locator.

  • A card proves its app-only runtime calls (#1415, the view-origin proof; the server measures it and refuses nothing at this release). Every tools/call of ggui_runtime_submit_action, ggui_runtime_sync_context and ggui_runtime_pull the card sends (a dispatch and its audits, the context mirror, the bridge’s pull) carries a v1 proof at params._meta["ai.ggui/view"] when the card holds a view key root for the call’s session. It sends the JSON-normalized arguments it signed. A dispatch signs whether the gesture carried user activation (measured, never enforced). Any other tool is sent unchanged. Signing is synchronous, through @noble/hashes (a new dependency of @ggui-ai/iframe-runtime), and total: a card that cannot sign sends the call without a proof. A dispatch answered VIEW_ORIGIN_UNPROVEN shows the ordinary error toast and never reads as a host that cannot relay. A relay forwards _meta["ai.ggui/view"] byte-identical. On a call made through the MCP Apps App, _meta also carries the MCP SDK’s own progressToken, as it already does today.

  • Added, in @ggui-ai/protocol (#1531, the live channel’s stream counter generation): StreamEnvelope.streamEpoch, AckPayload.streamEpoch and SubscribePayload.fromEpoch, all optional, and makeStreamEnvelope stamps a streamEpoch beside a seq, never alone. A new epoch means the session’s counter restarted and seq values may repeat: a client that dedupes by seq resets to nothing applied instead of dropping every frame, and a server holding another epoch than a resume’s fromEpoch replays everything it retains. An absent epoch is unknown, never a mismatch.

  • A card reads the stream counter’s generation (#1531). Its seq dedupe resets to nothing applied when an envelope or a subscribe ack names a different streamEpoch than the one it last saw, so a restarted counter’s frames are applied instead of dropped as repeats. A resumed subscribe sends fromEpoch beside fromSeq, in the WebSocket frame and as &fromEpoch= on the SSE stream, and never without it. A server that sends no epoch gets the earlier behaviour, including the reset on an ack whose streamSeq is below the cursor. Added: RegistrySseOptions.fromEpoch (@ggui-ai/live-channel), a getter read only when fromSeq gives a cursor.

  • ggui_runtime_submit_action, ggui_runtime_sync_context and ggui_runtime_pull verify a view proof when a view sends one at params._meta["ai.ggui/view"] (#1415), and name the verdict on their tool_invoked line. No call is refused or answered differently. The line carries viewProof (valid, missing or invalid). When the proof is not valid it adds viewProofReason, the caller’s claimedSessionId, and for a dispatch or a sync viewProofEra, read from the session’s row. When it is valid it adds viewProofSessionId, the proof root’s age and door, a bucket of the view clock’s skew, user activation on a dispatch, and viewProofRepeat when this server saw the same proof nonce for the session within 10 minutes. The proof, its nonce and its tags are never logged. These lines, and ggui_runtime_declare_tool_catalog’s, also carry authSource. A refused sync now shows ok: false and its code on its line. A server with MCP Apps off keys no view and does not verify proofs: its lines say viewProofUnverifiable: true, and it logs view_proof_unverifiable once at boot.

  • Added, in @ggui-ai/mcp-server-handlers (#1415, the view-origin proof; measured, never refused): SharedHandler.viewProof, which declares whether a tool’s calls carry a view proof and what it is for (ggui_runtime_submit_action requires one for kind: "dispatch" and measures the other kinds, ggui_runtime_sync_context requires one, ggui_runtime_pull measures one), with viewProofUseFor; HandlerContext.viewProof, the verdict the transport will put there; and HandlerContext.sessionRows (createSessionRowReads, readSessionRow), a per-request memo so a proof gate and the handler read a session’s row once. ggui_runtime_submit_action and ggui_runtime_sync_context now declare VIEW_ORIGIN_UNPROVEN on their closed output schemas in tools/list. No server answers with it at this release: a client that cached the schema will accept it when a later release does.

  • Added, in @ggui-ai/protocol/integrations/mcp-apps (#1415): VIEW_ROOT_SRC (['result', 'read']), ViewRootSrc and isViewRootSrc, the one list of doors a view root’s src may name. It is a closed set: a verifier refuses any other value, so a new door is added to the list one release before any door mints with it. ViewRootClaims.src and, in @ggui-ai/mcp-server-core, WsTokenClaims.src are typed ViewRootSrc.

  • Added, in @ggui-ai/mcp-server-core (#1415, the view-origin proof’s server side; no door calls it yet): mintViewRoot, a root ws token stamped with the signing secret’s key id and the issuing door (kid, src), and the view key derived from its payload, or viewKeyNotIssued: 'oversize' when the payload is too long for a proof; defaultViewKid, deriveViewKey, viewRootFits; verifyViewProof, which reads the proof from the request’s _meta and names the first failing check (missing: meta_absent or key_absent; invalid: the grammar, unknown_key, bad_mac, session_mismatch, app_mismatch, args_mismatch) and never throws (an internal failure is verifier_error, with the class of what was thrown); and ViewProofRepeatCache, a bounded replica-local record of seen (session, nonce) pairs, O(1) per observation. WsTokenClaims gains the optional kid and src; tokens without them verify as before.

  • Added, in @ggui-ai/protocol/integrations/mcp-apps (#1415, the view-origin proof, not yet used by any server or runtime): the request key MCP_APP_AI_GGUI_VIEW_META_KEY (ai.ggui/view), the relay door shape VIEW_PROOF_RELAY_SHAPE, the v1 grammar VIEW_PROOF_V1_PATTERN, the labels and size bounds, the bound-argument table VIEW_PROOF_V1_BOUND_ARGS, the byte builders viewKeyInputBytes, viewProofArgsBytes and viewProofCallBytes, parseViewProof and formatViewProofV1, the result code VIEW_ORIGIN_UNPROVEN, and a known-answer vector VIEW_PROOF_V1_VECTORS. The module holds no crypto; a signer and a verifier HMAC these bytes and reproduce the vector.

  • A live-channel subscribe without fromSeq no longer carries replayTruncated: true on its ack when the stream buffer reports its reserved-channel walk truncated, for example once a session’s retention has expired. AckPayload.replayTruncated is documented as absent on fresh subscribes, and a fresh client has no history that could have a gap.

  • A live-channel subscribe that fails after the server registered the subscriber, such as a failed event-ledger read, no longer leaves that subscriber behind. The client may already hold the ack, so the server unregisters the subscriber and ends the transport: close code 1011 on the WebSocket, the stream ended on SSE. The client re-subscribes with its ordinary backoff and gets a fresh replay, instead of running on a partial one with no signal. The first page of the ledger is read before the ack, so a read that fails sends no ack, and a client that resets its retry budget on an accepted ack still backs off to its cap. It is not 1012, whose retry comes at once, because a failure that repeats for one session would then become a tight reconnect loop. Before, an SSE subscriber was never unregistered, and a WebSocket stayed half-subscribed. (#1528)

  • The live channel keeps its order, ack → replay → live, on a subscribe that asks for the event ledger (sinceSequence), which every SSE open does. The server awaits the ledger read before writing the replay, and a live frame arriving in that window, published on this server or delivered from another one, used to reach the client first, ahead of replay frames with lower seq. A client that dedupes by seq then dropped the replay. Such frames are now held and sent after the replay. (#1525)

  • SPEC erratum (§12.2.1), with no wire change: a subscribe without fromSeq replays no channel the render declares, as before, but the server SHOULD send the retained envelopes of the reserved _ggui:* channels right after the ack, at seq at or below the ack’s streamSeq, as both reference servers already do (so a late viewer sees the _ggui:preview skeleton). A client MUST apply those frames, and dedupe against the highest seq it has applied, never against streamSeq. The old text said a fresh subscribe replays nothing and told clients to seed lastSeenSeq from streamSeq; a client that did so dropped the reserved replay. SubscribePayload.fromSeq, AckPayload.streamSeq and StreamEnvelope.seq in @ggui-ai/protocol are re-documented to match. (#1521)

  • @ggui-ai/protocol/wire exports the Plane-2 reader, so a browser can read a tool’s <code>: <detail> error text: parseDomainErrorText, ParsedDomainErrorText, isDomainErrorCode, DOMAIN_ERROR_CODES and DomainErrorCode. The reader carries only the code names (about 0.6 KB minified), not the registry’s descriptions: the names now live in a module of their own, and the registry rows are typed exhaustively over them. The root exports are unchanged. Changed (VERSION-POLICY §3.7, compatible): DOMAIN_ERROR_CODES is a readonly tuple, in the same order, instead of a readonly DomainErrorCode[].

  • ggui_runtime_sync_context answers a session read that fails exactly as it answers a session that does not exist, SESSION_NOT_FOUND, as ggui_runtime_submit_action already did with PIPE_NOT_FOUND, instead of throwing the store’s error. It still writes nothing. The cause is logged on the runtime_ownership_unverified line, which now carries the store’s error text. The cross-app class contract test gains a failed-read arm: both runtime write tools must answer it byte-identically to a missing session, and every other session-keyed tool is pinned as it answers today (they throw). (#1514)

  • A relay that retries a ggui_runtime_submit_action dispatch with the same actionId (for example after losing the response) no longer writes a second user.submitted ledger row. The dispatch appends to the pipe first and writes the ledger row only when the pipe stored the gesture; the retry is logged as submit_action_duplicate_dispatch. The oneShot spend is applied on a retry too, because recording it twice is one spend, so a gesture whose first call failed between the append and the spend is still spent. Changed (VERSION-POLICY §3.7, compatible): PendingEventConsumer.append (@ggui-ai/mcp-server-core) resolves 'appended' or 'duplicate' (PendingEventAppendOutcome). An adapter that resolves nothing still conforms and is read as 'appended', which keeps the earlier behaviour. The published runPendingEventConsumerConformance suite asserts the outcome. (#1517)

  • The self-contained shell (the per-render document a resources/read of a ui://ggui/render/… locator returns) now reports a runtime bundle that fails to load: it posts one ggui:bootstrap-failed with BUNDLE_FETCH_FAILED to the host, after retrying a content-hashed URL’s unhashed twin, or at once for any other URL, as the thin shell already did. Before, the host heard nothing. The card itself is unchanged: it keeps its loading mark (or stays blank without one), and only the host is told. The runtime <script> of every such shell now carries data-ggui-runtime="src" beside a small inline listener, so its bytes change. (#1503)

  • ggui_ops_register_blueprint (and createRegisterGeneratedBlueprint, which shares its core) and ggui_ops_generate_blueprint mirror a registration into the cache under the durable row’s own id. A card served from that cache row records that id, and the store a re-mint reads now holds the row and its body under it, whether that store is the ops store itself or a separate one the mirror writes through to. So re-minting the card’s locator finds the blueprint instead of reporting it gone. A mirror that writes through to the ops store itself no longer leaves a second durable row per registration. Cache rows bound before this keep the id they were served under. (#1497)

  • The live channel no longer mints a reconnect sessionToken into the subscribe ack (#1488). No server ever verified that token, so on the bearer reconnect it was documented for it proved nothing of its own, and no client read it; a reconnect presents a live wsToken again. AckPayload.sessionToken stays declared, optional and @deprecated through this release, and is removed in the next (VERSION-POLICY §3.6). The render_channel_subscribed log now reports the credential a subscribe came on: bootstrap (true for a wsToken, on the WebSocket or the SSE stream) and source (ws_token, console_cookie, or the auth adapter’s source).

  • Removed (a pre-1.0 minor, VERSION-POLICY §3.7), each with no replacement: mintSessionToken and DEFAULT_SESSION_TOKEN_TTL_SEC (@ggui-ai/mcp-server-core), and GguiSessionChannelBootstrap.issueSessionToken (@ggui-ai/mcp-server). TokenKind keeps 'session', deliberately, until the release that removes AckPayload.sessionToken: kind is a claim inside tokens that cross the wire, and a session token an earlier release minted must still be refused as wrong_kind.

  • Removed (a pre-1.0 minor, VERSION-POLICY §3.7), each with no replacement: the variant selector, which nothing called. In @ggui-ai/mcp-server-core: selectVariantWithLlm, encodeSelectedReason, preFilterCandidates, computeVariantSelectionCacheKey, the VariantSelectionContext, VariantSelectionDecision, VariantSelectionCache, VariantSelectionCacheEntry, VariantSelectionPickFn, VariantSelectionResult and VariantSelectorWithLlmOptions types, the DEFAULT_VARIANT_SELECTION_CONFIDENCE_THRESHOLD, DEFAULT_VARIANT_SELECTION_CACHE_TTL_SEC and DEFAULT_VARIANT_SELECTION_SHORTLIST_SIZE constants, BlueprintSelector, BlueprintSelectorContext and createDeterministicBlueprintSelector, and on /in-memory InMemoryVariantSelectionCache and InMemoryVariantSelectionCacheOptions. In @ggui-ai/mcp-server-handlers: the optional HandshakeNegotiator.selectVariant method. In @ggui-ai/mcp-server: createGguiServer’s blueprintSelector option and GguiServer.blueprintSelector. None of these types a wire member. BlueprintMeta.selectedReason, the handshake.decided event and extractSelectionConfidence stay: a conf=<n> suffix on selectedReason is still read into the event’s selectionConfidence, and nothing in @ggui-ai/* writes one.

  • Removed (a pre-1.0 minor, VERSION-POLICY §3.7), with no replacement: GenerationDeps.resolveLlmCaller in @ggui-ai/mcp-server-handlers (#1625). Nothing read it: the render handler resolves the blueprint the handshake decided, and the matcher’s judge is wired through the handshake negotiator. It does not type a wire member. GenerationDeps.installedBlueprints stays, because createGguiServer forwards it to the handshake negotiator.

  • Generations now stamp the durable blueprint record with the build that made them (Blueprint.build, declared in 0.25.0): a ggui_render cold generation and ggui_ops_generate_blueprint write the generation’s build, and so does createRegisterGeneratedBlueprint when its caller passes the generation’s build (GeneratedBlueprintBytes.build?), on the durable row and on the row a cache mirror writes through to. A dedup keeps the row’s first stamp. Rows registered through ggui_ops_register_blueprint, installed and seed-imported rows carry none: the operator door takes no build (its schema names none, and over MCP an unknown build is stripped before the handler runs). admitGeneratorBuild (@ggui-ai/mcp-server-handlers/renders) is the one admission rule: a stamp is kept only on LLM-generated code and only when it passes generatorBuildSchema, else it is dropped and the row registers unstamped. RegisterBlueprintInput gains build?. The conformance kit gains an ops-list-blueprints n1-compat wire. (#1280)

  • ggui_consume now confirms the session belongs to the caller before it registers as that session’s consumer. Before, a consume refused for another app’s session counted as that session’s consumer while its store read ran, and its end stamped the session’s last consumer exit, which changed how long that session’s dispatches waited for a consumer over the next minute. InMemoryActiveConsumerRegistry also forgets a consumer’s exit after 60 s (ACTIVE_CONSUMER_EXIT_RETENTION_MS, exported from @ggui-ai/mcp-server-core/in-memory), so it keeps the last minute’s exits instead of one per session that ever had a consumer; msSinceLastExit reads undefined past that. ggui_runtime_submit_action waits as before, except for a session re-rendered in place after its consumer exited (on the in-memory and SQLite stores), which now waits as a fresh render does for 60 s; it also reads the session row once per dispatch instead of twice. (#1485)

  • Snapshot-read renewals are bounded (ggui#1496). /api/sessions/<id>/state still renews the wsToken it is given, but each renewal carries its chain’s root forward as a new optional rootIat claim, and its expiry never passes rootIat plus the refresh window. A chain of renewals therefore ends one window after its root was minted, and /state, /events, /stream and the live channel all refuse its last token in the same second, as long as every server sharing the secret uses the same window and it has not shrunk. After that, a new credential needs an authorized door, such as ggui_runtime_refresh_ws_token, which mints a new root for a caller admitted to the session. A token without rootIat (every earlier token) is its own root.

  • Added: createGguiServer options wsTokenTtlSec (default 180, the lifetime of the ws tokens the server mints; a snapshot-read renewal gets less when its chain’s window ends sooner) and wsTokenRefreshWindowSec (default twice the TTL; the factory throws when it is shorter than the TTL); WsTokenClaims.rootIat? and MintWsTokenInput (mintWsToken’s input, with rootIat?) in @ggui-ai/mcp-server-core.

  • Changed (VERSION-POLICY §3.7): WsEnvelopeVerdict.iat (@ggui-ai/mcp-server-handlers) is replaced by rootIat, the chain root’s issued-at (a renewal’s rootIat claim, else its iat), so the refresh log’s rootAgeSec ages the chain rather than its last link.

  • A render’s postSuccessHook now receives the token counts the generation reported, on generation.usage (GguiSessionPostSuccessArgs, @ggui-ai/mcp-server-handlers): inputTokens (the input not served from a prompt cache), outputTokens, and cacheReadTokens / cacheCreationTokens when the provider reports them. With @ggui-ai/ui-gen’s generator those are its coding turns and any in-loop text-evaluation rounds, summed; the in-loop visual judge’s calls are not included. generation stays null on a blueprint reuse. (#1513)

  • A card that receives live updates through the host’s tools/call bridge (ggui_runtime_pull, the path for hosts whose iframe can’t reach the server) now pulls again as soon as a pull returns, as the bridge’s subscription mode was designed to. It no longer waits 15 seconds after every successful pull, so an update landing between pulls reaches the card up to 15 seconds sooner. After three pulls in a row that return nothing, the card still drops to pulling every 15 seconds. (#1527)

  • A card whose live credential (wsToken) expires now renews it through its host instead of going quiet. It renews at mount, when the slice it booted from has expired. It also renews mid-session: when the server refuses the credential as expired (BOOTSTRAP_EXPIRED), when HTTP polling answers 410, or when the card falls back to the host’s bridge after its expiry. It calls ggui_runtime_refresh_ws_token once per credential through the host’s tools/call relay. It then moves its live channel onto the new credential and resumes from the last update it applied. A card whose session is confirmed gone, or whose host confirms it cannot relay tools/call, says so and stops pulling, keeping what it painted. Stream envelopes (data) are now applied once each by their seq, and a resumed subscribe passes the last one applied as fromSeq. A subscribe ack whose streamSeq is below that cursor shows the server’s counter restarted. The card then forgets the cursor instead of dropping the new envelopes as repeats. (#1496)

  • Changed (VERSION-POLICY §3.7): in McpAppAiGguiMetaParseResult (@ggui-ai/iframe-runtime), the ok arm gains held?, the expired live credential the parse now keeps instead of discarding. The EXPIRED_BOOTSTRAP arm now carries meta and held. HeldCredential is exported. (#1496)

  • Added: RegistrySseOptions.initialSinceSequence (@ggui-ai/live-channel) also accepts a function, read when the SSE rung connects. The new RegistrySseOptions.fromSeq getter appends the stream cursor (&fromSeq=) to every connect. (#1496)

  • inLoopEvaluation and runsInLoopEvaluation (@ggui-ai/ui-gen) name which in-loop evaluation legs a generation runs: the text evaluator when evaluation.enabled, the visual judge when visualEvaluation.enabled or qualityConfig.visualEval. They are the harness’s own gate, so a host pricing a generation’s usage can tell whether those counts may include evaluation calls, with the options it configured the generator with. (#1513)

  • A card’s WebSocket counts a subscription as accepted only once it has held for 5 seconds. A server whose subscribe fails right after its ack, and closes, no longer resets the card’s retry budget on every attempt. The card backs off as for any failed attempt and, after the usual ten retries, moves to its next transport. It used to retry every second indefinitely. (#1533)

  • The generation’s cost cap (qualityConfig.maxCostPerGeneration, @ggui-ai/ui-gen) now counts all of a generation’s spend, so under the same cap the eval loop can stop sooner than before. It now counts the in-loop visual judge’s calls, including a configured criteria bank’s report-only calls (the cap bounds money spent, so these count toward it, through their cost and never their answers), and the prompt cache’s reads and writes, priced at the model’s cache rates. A model with no cache-write rate is priced at the largest write premium the model registry states, so a write is never counted under. Added: the generation result carries the in-loop visual judge’s spend as its own part, inLoopVisualTokens (beside tokens, never inside it).

  • A tap the card’s own action contract refuses is no longer silent (#1536). The runtime posts an action-refused observability event to the embedding host (ggui:observe), naming the render, the action and each violation by its field path and schema keyword, never by value. It posts at most ten per render, and marks the tenth capped: true. The visitor sees the ordinary error toast, named by the action’s declared label. Nothing is sent, as before. Added: ActionRefusedEvent (@ggui-ai/iframe-runtime), and, in @ggui-ai/wire, BuildWireConfigOptions.onDispatchRefused with its payload DispatchRefusedInfo, called after onViolation with the refused action’s name.

  • ggui’s own relays carry a view’s proof, and refuse a view’s call to a model-only tool (#1415). @ggui-ai/agent-server’s POST /agent {kind:'tool-call'} takes an optional meta, the view’s request _meta, and forwards only its ai.ggui/view string, verbatim, as the relayed call’s _meta. A call to a tool whose declared _meta.ui.visibility lacks app (ggui_render, ggui_update) is answered -32602 Unknown tool: <name> and never relayed. The relay reads each server’s tools/list at most once every 5 minutes, and once more for a name it has not seen. When a re-read fails, it keeps answering from the last list it read and logs relay_tool_visibility_stale. A tool it has never seen while the list cannot be read is answered -32603 tool visibility unavailable. In @ggui-ai/mcp-apps-react-native, onToolCall gains an optional third argument, McpAppViewCallMeta (the proof alone), and <McpAppIframe> gains a toolVisibility prop that refuses the same calls. The ggui-basic-web sample sends its view’s _meta to the relay. Added, in @ggui-ai/protocol/integrations/mcp-apps: toolVisibleToApp, the app-side twin of toolVisibleToModel.

  • A card never moves its stream cursor back to an epoch it has left (#1531). It remembers the last four epochs it left, drops their stamped frames and ignores their acks, as the server does. Before, a late frame of the old generation moved it back with nothing applied, and the next frame of the new generation moved it forward again with an empty floor, so a duplicate in the new generation could pass. An unstamped frame still always passes, and no longer moves the epoch.

  • ggui_ops_delete_blueprint takes a blueprint out of reuse (#1541). ggui_ops_register_blueprint and ggui_ops_generate_blueprint write a blueprint twice: its durable row, and a mirror in the cache the handshake’s exact-key probe matches from. Delete removed only the durable row, so the next handshake on the same contract still reused the deleted blueprint. It now removes the cache entry (its vector and its exact-key binding) first, then the durable row. A cache delete that fails fails the call with the row intact, so a retry completes it. Re-running a delete for a blueprint whose row an earlier delete already removed clears its leftover cache entry, in the caller’s own app only. Added: GguiOpsDeleteBlueprintDeps.cacheRegistry (@ggui-ai/mcp-server-handlers), which createGguiServer binds to the same registry as the writers.

  • The ggui dark theme’s muted text is dimmer: onSunken is #a3a3a9 (6.57:1 on a card, 5.80:1 on a sunken well), where it was #d9d9d9, close enough to the primary text that secondary text did not read as secondary (#1566). Six components drew text you read with the muted role, in both themes, and now draw it with onContainer: the secondary and ghost Button labels, the option labels of Checkbox, RadioGroup and Toggle, Accordion answers, Table body cells, a Select’s displayed option, and the other party’s messages in ChatWindow. What is muted stays onSunken: option descriptions, the RadioGroup field label, the Table header and caption, a Select placeholder while it shows, and the other party’s timestamps. In the light theme that text moves from #5a5a5a to the card’s ink.

  • Added, in @ggui-ai/ui-gen: visualEvaluation.actOn ('critical' or 'major', default 'critical'), which of the in-loop visual leg’s own findings may start another eval round when the quality mode is fast. With 'major', the visual judge’s major findings start a fix round as a failure does; the fit measurement’s warn-level overflow and the text evaluator’s warnings never count. The default decides exactly as before, and the other quality modes, which already act on every warning, are unchanged. A round that continues with the setting present logs why (continuing: fails=…, visual majors acted=… (actOn=…)). (#1542)

  • ggui serve keeps a session’s live-stream counter across a restart (#1534). A server that stores its sessions in SQLite, as ggui serve does by default when better-sqlite3 is installed (unless you pass --ephemeral), and whenever ggui.json declares storage.renders with driver: "sqlite", now keeps the live channel’s stream buffer in the same database: each session’s seq, its streamEpoch and the envelopes it retains for replay. After a restart, a session continues its seq in the same epoch, and a client that reconnects with fromSeq gets the frames it missed. Before, every restart started each surviving session’s seq again at 1 in a new epoch, with nothing retained. Added, in @ggui-ai/mcp-server-core: SqliteGguiSessionStreamBuffer (/sqlite), and runGguiSessionStreamBufferConformance (/contract-tests), the stream buffer port’s conformance suite, which the in-memory and SQLite buffers both run. resolveStorageFromConfig returns the buffer as streamBuffer, and createGguiServer keeps its in-memory default unless you pass one.

  • A retried ggui_runtime_submit_action dispatch is one gesture, promised on the wire (#1519, SPEC §11.1). A host or relay that could not confirm a dispatch may retry it: a server SHOULD answer a repeat of an earlier committed (sessionId, actionId) as it answered the first, with no second pending event, no second user.submitted row and no second spend, even when both requests are in flight at once. The promise becomes a MUST in the next release. @ggui-ai/mcp-server already behaves this way. An actionId reused for a different gesture (a different intent or actionData) keeps the first gesture. This release the second is still answered as a duplicate and dropped, so the view shows success, but no longer without a trace: it is logged as submit_action_action_id_reused, and nothing of it is spent. Its answer, ACTION_ID_REUSED, is declared in the tool’s output schema this release and emitted from the next, when the view shows the user an error toast (as on any ok: false); the second gesture still does not reach the agent. SPEC §11.1 names the promise’s two residuals. Agents keep deduping consumed events by actionId: the promise serves relays. Added: dispatchGestureBytes and ACTION_ID_REUSED (@ggui-ai/protocol/integrations/mcp-apps); PendingEventConsumer.append takes an optional gestureDigest and reports 'conflict' (@ggui-ai/mcp-server-core), and the SQLite pending-event store adds its digest column on open. If you run runPendingEventConsumerConformance against your own store, its three conflict cases run only when your factory sets reportsConflict: true (they are skipped otherwise, and become required with the MUST); the dispatch-idempotency conformance catalog (@ggui-ai/protocol-conformance/dispatch-idempotency-conformance), at SHOULD level.

  • ggui_runtime_submit_action’s description in tools/list, and its two PIPE_NOT_FOUND messages, no longer say the view falls back to ui/message (#1571). The view never did: on any ok: false it shows the user an error toast and sends nothing (SPEC §4.7). The texts now say the gesture was not enqueued. ggui_render also logs [ggui_render.pipe_open_failed] with its reason when a session’s pending-event pipe fails to open; before, that failure was silent, and the only sign was each later gesture on the session being refused PIPE_NOT_FOUND.

  • A blueprint row that copies another row’s bytes can say so (#1570). Blueprint.clonedFrom (optional) names the row it copies. The copy’s source, build and codeHash stay its original’s, which are true about the bytes, and nothing that serves reads the new field. It is declared this release and written from the next, because the previous release’s strict blueprintSchema would refuse a row that carries it. The portable form does not carry it. If you implement BlueprintStore, persist it verbatim: the kit’s store suite now grades that. Two things to know if you read rows: a row without it is not necessarily produced in your store (an imported row carries its source and no clonedFrom), and removing a copy is a row delete, never a content takedown, because a copy shares its original’s body.

  • Changed, in @ggui-ai/mcp-server-handlers: ggui_render’s checkRenderContracts seam now receives exactly the shape its type declares, { actionSpec?, streamSpec?, agentCapabilities? }, built from the render and its contract (#1579). It used to receive the whole committed session, with the contract’s agentCapabilities spread onto it. ComponentGguiSession declares no such field, so the committed session row carried an undeclared property that nothing read after the check. The row no longer carries it. If your own binding read anything else off its argument, read it from the render instead; the schema-compat check itself sees exactly what it saw before.

  • The default, aggressive and always MCP instructions presets (@ggui-ai/mcp-server) are corrected to match the shipped tools (#1579). Among the fixes: after consuming a gesture, the agent repaints the same card with ggui_amend (they said to render a new UI); an event names its action in intent and carries its payload in actionData (they described a nextStep on actionData); a render response carries a sessionId and a ui:// resourceUri, never a URL to show; the card itself posts the doorbell message when nobody is consuming; and ggui_list_sessions is described as offered only where a server registers it. ggui_render‘s tool description, and the presets, no longer tell the agent to read a fixBy field that no refusal carries: retry an after-fix refusal only once you have performed refusal.fix yourself. The presets’ opening rendering policy is unchanged.

  • Changed, in @ggui-ai/mcp-server-handlers: a credit ledger entry’s kind on ggui_ops_list_credit_transactions is an open vocabulary (#1532). The known kinds, now exported as KNOWN_CREDIT_TRANSACTION_KINDS, are free_credit, render_charge, topup and refund, and a deployment may add its own. The output schema declares kind as a non-empty string where it was a closed enum, CreditTransactionView.kind is a CreditTransactionKind (a known kind, or any other non-empty string), and the descriptions tell a reader to accept an unknown kind as named, never as a render charge. If your code switches on kind, give it a default arm.

  • ggui serve enables the console’s theme upload (#1579). The CLI built the uploader and never passed it to the server, so the upload answered 501 (upload_not_configured) under ggui serve, which its own message named as the way to get uploads. ggui serve also stops writing render-signer-secret.hex into the persistent directory. The server stopped reading it when render signing was removed with the /r/ viewer; an existing file is harmless and can be deleted.

  • An MCP instructions preset or custom string you set now reaches hosts, and setting none sends none (#1579). createGguiServer’s mcpInstructions (--mcp-instructions / GGUI_MCP_INSTRUCTIONS on ggui serve) was passed where the MCP SDK does not read it, so no server’s instructions ever reached a host; the mcp_instructions_set boot line was the only trace. It is now sent in the initialize result. The no-flag default is off: it was nominally default, never delivered, so an operator who set nothing sees no change. mcp_instructions_set and mcp_instructions_off now carry sentInInitialize.

  • Removed, in @ggui-ai/protocol and @ggui-ai/mcp-server-handlers: validatorScore on ggui_ops_generate_blueprint’s output (opsGenerateBlueprintOutputSchema) (#1579). It was read off the generator’s metadata through an undeclared field that no generator sets, so it was never populated, and no release ever returned it. A client that cached the old tools/list expects an optional field it never received. Blueprint.validatorScore on blueprint rows is unchanged.

  • In a fullscreen canvas, a band that runs edge to edge keeps its content on the card’s edge (#1556). The frame gives a card’s content a 16 px inset, and a band that bleeds (a surface="hero" band that opens the card, or any element that declares bleed) took that inset back for its content as well as its ground, so an opening band’s heading sat 16 px left of the body and chips below it. The band’s ground still meets the panel’s edge; its content now keeps the inset, so a card whose edges agree in the chat card agree in the panel too. Only the left and right edges changed; a band’s top and bottom are as before.

  • Added, in @ggui-ai/mcp-server-handlers: GguiSessionPostSuccessArgs.proposalServed (#1331), optional. ggui_render’s post-success hook now learns whether the render served the stored blueprint its handshake proposed, as proposed: true when the proposal was accepted and the proposed blueprint is what served, false on a re-aimed render (even to the same blueprint), a cold generation, a failed render, or a store that now serves a different card. It is the same predicate as the blueprint resolution event’s strategy: 'proposed', served: 'stored' and blueprintId equal to proposedBlueprintId.

  • Changed, in @ggui-ai/mcp-server-core: BlueprintIndex.deleteId(scope, exactKey, expectedId) takes the blueprint id the unbind is aimed at, and removes the binding only while the key still points at it (#1603). Deleting a blueprint, self-healing a dangling binding, and evicting a row each rebuilt a key and unbound whatever it held, so a key re-bound since to a newer design lost it, and the newer design dropped out of exact-key reuse. All four callers now pass the id they mean. A custom BlueprintIndex must remove the binding only when it points at expectedId (a no-op otherwise, and on a missing binding); runBlueprintIndexConformance grades it. A failed unbind after a delete or an eviction is now logged as blueprint_index_unbind_failed, where it was swallowed.

  • The operator-default mark now says what it does (#1611). ggui_ops_update_blueprint, ggui_ops_generate_blueprint, the ops schemas’ isOperatorDefault / setAsOperatorDefault descriptions (which ship in tools/list) and the console’s blueprint pages said the flag pins a blueprint, and the console said the handshake picks by it. The mark orders blueprint listings (the default first) and is shown to operators; it does not decide which blueprint serves a render, which the handshake resolves from the blueprint index. No behaviour changes.

  • A Select with a placeholder and neither value nor defaultValue now shows the placeholder, muted, until the user picks (#1569). The placeholder is a disabled first option, and nothing selected it, so the browser showed the first real option as if the user had chosen it, and a form reading the element’s value got that option. It now starts on the empty choice, and the text reads at body strength once a choice is made. A value or defaultValue still wins, and a Select without a placeholder is unchanged.

2026-09-28 — v0.25.0: four security fixes to cross-app access — upgrade if you run @ggui-ai/mcp-server — and a tapped control shows it is working

Section titled “2026-09-28 — v0.25.0: four security fixes to cross-app access — upgrade if you run @ggui-ai/mcp-server — and a tapped control shows it is working”
  • Security fix (@ggui-ai/mcp-server-handlers): ggui_runtime_submit_action and ggui_runtime_sync_context act only on a session that belongs to the caller’s own app, as ggui_runtime_pull already did. A call for another app’s session or a missing session is answered as not-found before any effect: PIPE_NOT_FOUND on ggui_runtime_submit_action, SESSION_NOT_FOUND on ggui_runtime_sync_context. A failed session read is answered PIPE_NOT_FOUND on submit_action; on sync_context it threw the store’s error, with nothing written, until the next release (#1514). (Corrected: this note first said PIPE_NOT_FOUND for all three cases on both tools.) If your host relays runtime calls under a credential whose app differs from the render’s (for example, on the universal endpoint with an identity-default app), those calls now get not-found. Relay them at the render’s app endpoint.
  • Security fix (@ggui-ai/mcp-server): on the live channel’s bearer path, a subscribe that declares an app other than the one its credential resolves to now needs the deployment’s per-app authorization to pass, through a new optional authorizeApp seam that createGguiServer fills from perAppRouting.authorize. Otherwise it is refused with APP_MISMATCH. The token and console-cookie paths are unchanged. If your clients subscribe with a bearer credential and declare another app, make sure perAppRouting.authorize admits that pairing.
  • Security fix (@ggui-ai/mcp-server-handlers), and a silent change: a ggui_render that names a session the caller can’t see (another app’s or another subject’s) now gets a fresh session id instead of writing into that session. The render succeeds under the new id, and the other session is never written. This applies only when a handshake negotiator sets target.sessionId. There is no error to catch, so if you pre-assign session ids, read the returned id.
  • Security (@ggui-ai/mcp-server), new and opt-in: perAppRouting.perAppOnlySources lists credential sources that are valid only at their own app’s endpoint. Such a credential is refused on an MCP mount without an app in its URL, and authorized on the live channel.
  • The conformance kit does not yet grade cross-app refusal, so the four fixes above are verified by the packages’ own tests, not by the kit.
  • ggui_runtime_refresh_ws_token is now an authorized re-mint. It re-mints a view’s live credential, at any age, only for a caller the render read door admits to the envelope’s session (the app, then the subject). A session the caller cannot see gets the same not-found error ggui_runtime_pull throws, and there is no refresh window. Its advertised output schema is unchanged.
  • Added: withWsToken(url, wsToken) in @ggui-ai/protocol (swaps the token in a token-bearing session-API URL); verifyWsTokenSignature in @ggui-ai/mcp-server-core (a ws envelope’s signature and kind, at any age); renderReadAllowed and RenderReadRowView exported from @ggui-ai/mcp-server-handlers/renders; and defaultHandlers’ option render.wsTokenVerify.
  • Changed: GguiRefreshWsTokenHandlerDeps is now { renderStore, verify, mint }.
  • Removed (a pre-1.0 minor, VERSION-POLICY §3.7). Before 1.0, a minor release may remove an export without a deprecation window. A caret range such as ^0.24.0 never picks up a new minor, so nothing changes for you until you raise your range:
    • refreshWsToken, RefreshWsTokenOptions and RefreshWsTokenResult (@ggui-ai/mcp-server-core), replaced by verifyWsTokenSignature plus the handler’s gate and your minter;
    • the 'refresh_window_closed' member of VerifyTokenFailure, with no replacement;
    • WsTokenRefreshSeam (@ggui-ai/mcp-server-handlers), replaced by GguiRefreshWsTokenHandlerDeps’ verify and mint;
    • GguiSessionChannelBootstrap.refresh and GguiSessionChannelBootstrapRefreshResult (@ggui-ai/mcp-server), with no replacement, because the channel never called it;
    • defaultHandlers’ option render.bootstrapRefresh, replaced by render.wsTokenVerify.
  • A card whose runtime bundle URL carries a content hash that no serving replica has (during a rolling deploy, after a rollback, or on a replayed card) no longer fails at once: both shells retry the bundle’s unhashed twin, same origin and path, one time (the thin shell then reports BUNDLE_FETCH_FAILED as before; the self-contained shell still reports no bundle failure, tracked as ggui#1503). New in @ggui-ai/protocol: runtimeBundlePlainTwin, runtimeBundleHashedNameSource, and the runtimeBundlePlainName option on gguiShellHtml. The server logs runtime_bundle_plain_served for each serve of the unhashed name while the hashed name is mounted.
  • @ggui-ai/mcp-server-handlers exports createRegisterGeneratedBlueprint, an in-process entry (not an MCP tool) that registers engine-generated component code together with the authored source it was compiled from. It records the generation’s own llm provenance (validated; no default) and hands the source to the blueprint registry, so a render that reuses the registration serves it through ggui_get_render_source and can be saved to a library. The public ggui_ops_register_blueprint input is unchanged and still takes no source, because it cannot verify that supplied source compiles to the supplied code. attachAuthoredSource adds that source to a registration made from compiled code alone, only when the given code is byte-for-byte what the registration serves, and has a dry-run mode that writes nothing. It reads its write back, and reports lost_race rather than attached when a concurrent rewrite of the row erases the hash. Alongside them, a blueprint registry wired with a code store but no durable blueprint store now writes the authored-source body its sourceCodeHash points at; before, the hash was recorded with nothing behind it.
  • ggui_amend, ggui_update and ggui_render log their session on the tool_invoked line, as the runtime tools do, so a reaction joins the tap that caused it by session instead of by app and time order. Amend and update log the session they app-scope-gated (the caller’s input rides an error line as claimedSessionId); render logs the session id it minted, on a rendered and on a failed result, and none on a refusal (the id is the call’s, not proof that a row backs it).
  • GguiSessionPostSuccessArgs.generation gains effort? — the named level the generation applied, as the engine reported it (GenerationMetadata.effort) — so a post-success hook reads the level that ran rather than a stored profile. Absent when no level was applied; null generation (a reuse or a failed generation) is unchanged.
  • The durable render identity record gains blueprintIdentity?: 'ephemeral', set when the render served an ephemeral blueprintId; RenderIdentityStore implementations must persist it (a new conformance case), and a ui://ggui/render/… re-mint of such a render answers BLUEPRINT_UNRESOLVABLE naming the ephemeral identity instead of reporting a blueprint as gone.
  • The ephemeral blueprintId covers every fresh generation whose key is already bound to DIFFERENT code — a forceCreate, a render that took no index read (e.g. a deployment with no reuse negotiator), or one that lost a race — not only a forceCreate. When the regenerated code is byte-identical to the bound blueprint’s, that blueprint’s id is returned (identity: 'existing').
  • The ggui_render result declares an optional effort member — the named effort level the generation that produced the render ran — and GenerationMetadata gains effort?, set by ui-gen when a named level’s dials apply. Declared now, sent from the next release (the result’s schema is closed at tools/list).
  • A forceCreate render at an occupied key now serves an ephemeral, content-addressed blueprintId of its own instead of the incumbent’s; the onBlueprintResolution event’s identity gains 'ephemeral' (a self-hosted sink that switches on the three earlier values must add an 'ephemeral' arm).
  • The durable blueprint record declares its minting engine’s build (Blueprint.build?: GeneratorBuild, generatorBuildSchema, and build? on the portable blueprint; GeneratorBuild now lives in @ggui-ai/protocol and @ggui-ai/mcp-server-core re-exports it). Declared now, emitted next release: no server stamps a row yet, and a row without a stamp reads as mintedBy: unknown.
  • ggui_consume now sends the nextStep it declared in 0.24.0. When the drain returned events, the result carries a ggui_amend({ sessionId }) hint to repaint the same card in place instead of rendering a new one, and its text leads with the same copy-paste example. It is absent on an empty drain and on a late drain from an expired render.
  • MODELS.openrouter lists openai/gpt-6-sol, openai/gpt-6-luna and anthropic/claude-opus-5.5, so they autocomplete as OpenRouter routes; every <author>/<model> string was already accepted by shape. They were listed after an OpenRouter tools smoke passed (3 of 3 tool calls returned per id with tool_choice: required, 0 errors); Opus 5.5 followed the two GPT-6 ids once its dotted id resolved to a price row.
  • A repaired handshake draft keeps what you declared on its actions. When a draft fails validation and the server repairs it (suggestion.origin: "synth"), the proposal now carries every member you declared on each action entry — oneShot, confirm, nextStep, description, example, icon — changing only what the findings name. Before, a repair rebuilt each action from its label and schema alone, so a card whose draft tripped any validation error was served without its oneShot and rendered with no one-shot guard, and nothing said so. Anything the repair cannot keep is named at its own path: a declared member the repaired contract cannot carry (a nextStep whose tool the repair did not re-declare) as a REPAIR_MEMBER_DROPPED finding at actionSpec.<name>.<member>, an action the repair renamed or removed as REPAIR_ENTRY_DROPPED at actionSpec.<name>, each with the gate’s reason in its message; a member the gate refused on your draft is named by that CTR_* finding at the same path, and a salvaged subset names each cut with the gate’s code at the cut path. The deterministic normalization tier, which never calls a model, keeps the same members; its allowed keys are now derived from the protocol’s entry schemas instead of a hand-kept list, so a stream entry’s mode, replay, complete and example survive it too.
  • The handshake suggestion’s blueprintMeta.matchedIntent, declared in 0.24.0, is now sent. On a judged semantic hit from the app’s own pool it carries the matched card’s stored intent, at most 280 characters, so an agent can compare it with its own intent before accepting the reuse. It is absent on an exact-key hit, where no judge ran, and on a hit from a pool shared across apps.
  • ggui_runtime_submit_action now checks a dispatch against the card’s actionSpec, as the live channel already did. An undeclared action, or a payload the declared schema rejects, is answered as a normal tool result, { ok: false, code: 'CONTRACT_VIOLATION', message, violations }, and never reaches ggui_consume. A card that declares no actionSpec is not checked, on either path.
  • Hosts built on @ggui-ai/agent-server offer the model only the tools it may be offered. Each MCP tool’s declared _meta.ui.visibility is now read, and a tool whose visibility lacks "model" (the six app-only ggui_runtime_* tools) is no longer declared as the agent’s capability. listModelVisibleTools({ url, bearer }) returns one server’s model-visible tool names for your agent SDK’s tool filter, and the Google ADK, OpenAI Agents and Claude Agent samples now use it. The Claude sample’s default allowlist is derived the same way instead of kept by hand, and an explicit allowedToolsByServer still wins.
  • @ggui-ai/ui-gen/check exports generateSampleProps, the props a render check falls back to for a contract: each prop’s example, then its default, then a value filled from its schema type.
  • A handshake can use a judge other than the LLM caller. HandshakeDecisionAdapter in @ggui-ai/mcp-server-handlers/renders takes an optional resolveRerank(ctx) that returns a RerankPair (a judge with the threshold it was measured on, now a named export), passed to the matcher as its rerank pair; without it, matching is unchanged. @ggui-ai/negotiator exports RERANK_SYSTEM_PROMPT, the match / no-match definitions the built-in judge asks, so another engine’s judge can ask the same question.
  • A card now reads the host’s answer to its wake-up message. When a tap needs a new agent turn, the card sends the host a ui/message and says it was sent to chat; if the host refuses that message in-band, the card now says so and asks the user to send a message to continue, instead of leaving a tap that silently did nothing. The refusal is recorded as a doorbell.refused event with the host’s error code. The tap itself was never lost: it is already queued for the agent.
  • Text in a card that isn’t wrapped in <Text> is now set at the theme’s body size, --ggui-font-size-base, instead of the browser’s 16px. On a theme whose body size is 1.1rem, a plain paragraph now reads 17.6px, as <Text> already did. Every built-in theme’s body size is 16px, so a card on a built-in theme looks the same. Cards that @ggui-ai/mcp-apps-react renders into your own page take the same rule, so their plain text follows the card’s theme rather than your page’s font size. cssTokensForAppTheme in @ggui-ai/ui-gen sets the same size on the judged page’s body, so a visual judge reads text at the size a visitor sees.
  • @ggui-ai/negotiator’s LLMCaller has an optional callStructuredMetered, which returns { value, usage? }: the tool input, with the call’s token usage as the provider reported it. The rerank judge uses it when a caller has it, so RerankDecision.tokenCost carries real token counts. tokenCost is now optional. Absent means the call wasn’t metered: the caller has no metered method, the provider reported no usage, or the call threw. { input: 0, output: 0 } now means only that no provider call was made. Code that reads tokenCost directly must handle it being absent. Existing LLMCaller implementations compile unchanged; TokenUsage and Metered are exported.
  • @ggui-ai/mcp-server’s Anthropic caller implements callStructuredMetered, so a handshake your server judges with an Anthropic model reports the response’s token usage as the decision’s tokenCost. A response with no readable usage reports none, never zeros.
  • @ggui-ai/mcp-server-handlers/renders exports generationInputsForHandshake(record, resolved): the input a ggui_render of a stored handshake hands its generator, minus the render’s own sessionId. It comes with the steps it is built from, storyForHandshake, generatorInputForStory, appGadgetsForContract and fetchGadgetTypes. The render calls the same functions, so a server that starts that generation before the render arrives derives the same input. A render that sends an override or infra derives with them.
  • A handshake that carries a contract now searches your saved interfaces the way they were saved: its query embeds the contract summary with the intent, as every stored interface’s vector does, instead of the intent alone. Its similarity scores therefore sit on a higher scale, so they get their own floor of 0.50 before the reuse judge. A handshake with no contract searches by intent alone as before and keeps the 0.2 floor. minCosineForRerank still overrides both. The 0.50 floor was chosen by a replay of 28 labelled request pairs through the real matcher under two embedding models, as the highest value that finds every match found today without adding a wrong reuse.
  • The with-guuey samples move to @guuey/* 0.30.0 and @silverprotocol/* 0.11.0 (AgJSON 1.0.0-draft.8). @ggui-ai/agent-server and @ggui-ai/mcp-apps-react run their contract tests against the same core; it is a development dependency of both, so nothing they install changes. The AgJSON version check stays major-only, so earlier 1.0.0 drafts are still accepted. In core 0.11.0, AgJSON’s reasoning effort is an open string (unrelated to ggui’s effort levels, which stay a closed set), a memory record may carry _meta, and prompt.blocked may carry reasonRaw.
  • A card that has its component and still shows nothing now says so. After each commit, the runtime checks what it drew. The host’s component-empty observability event gains three reasons: rendered-nothing (the component drew no element), detached (the tree landed in a part of the page that is no longer shown, because another mount took over the card), and no-commit (the card never finished drawing within five seconds, for example a component that suspends with no Suspense boundary). Each is reported once per blank, with a console line naming it. Before, these blanks were silent on every channel. If your host switches on the reason, treat an unknown one as a blank of unknown cause.
  • @ggui-ai/wire exports useActionPending(actionName): true from the moment a card’s action is sent until the agent’s answer lands on the card (a props update, an amend or a new render for the session), or 20 seconds pass. A control can show it is working (disabled, aria-busy, “Sending…”) instead of looking untouched while the agent reacts. The iframe runtime provides it, and a card’s import { useActionPending } from '@ggui-ai/wire' resolves. A dispatch the contract validator refuses, or a repeat of a spent one-shot action, never reads as pending. buildWireConfig takes the matching pendingInputsChanged notice and an optional actionPendingBoundMs.
  • Generated cards use it. Every control that sends an action (a button, a select whose change sends, an input that sends on Enter) is disabled and marked aria-busy from the moment it sends until the agent’s answer repaints the card, and a button shows a working label (“Saving…”). Only the control that sent the action looks busy; other controls of the same record may be disabled, and everything else stays enabled.
  • Breaking, if your server bound them: @ggui-ai/mcp-server-handlers no longer ships the orgs, connector-keys and coupon handler families (the ./ops-orgs, ./ops-connector-keys and ./ops-coupon subpaths and their root exports), and createGguiServer no longer takes opsOrgs, opsConnectorKeys or opsCoupon. Organisations, wallets and user API keys are features of the hosted service, whose eleven tools keep their names and shapes there. SINGLE_CALL_OPS drops their three reads (ggui_ops_list_orgs, ggui_ops_list_connector_keys, ggui_ops_get_org_balance); a server that registers its own tools under those names passes them in control.singleCallOps. The apps family (opsApps) stays open, and its tool descriptions no longer describe the hosted service. A deployment that bound a removed family can stay on v0.24.0, or copy the handlers into its own code and register them through createGguiServer’s handlers option.
  • Text blocks carry an optional phase: "interim" marks narration between tool calls, such as “Looking that up…”, and anything else or absent is answer text. The thread routes and the invoke client keep it.
  • A tap on a card no longer shows the visitor the action’s internal name or its data. While it is sent, the card announces “Sending…” to assistive technology only, named by the action’s declared label when it has one. When the host takes the wake-up message, nothing is drawn over the card. Only a tap the visitor must act on draws a notice: the host cannot receive a message, or refused it.
  • A card’s live channel no longer reconnects forever when the server refuses its credential. @ggui-ai/live-channel’s ChannelRegistry and WSTransport take an optional classifyFrame, typed FrameClassifier, which returns a FrameVerdict. A frame you class 'accepted' resets the reconnect budget, and one you class 'refused-terminal' fails the WebSocket at once, so the registry moves to its next transport. Without it, behaviour is unchanged. The iframe runtime supplies one.
  • @ggui-ai/mcp-apps-react-native’s theme provider accepts React Native 0.82 and later, whose useColorScheme() can return 'unspecified'. A consumer on RN 0.82+ no longer fails to typecheck, and 'unspecified' counts as no system scheme: the colorScheme prop wins, and without it the provider uses 'light'.
  • If you use @ggui-ai/ui-gen’s visual evaluator, scores on the inline (xs) canvas move. The card is captured at its natural height inside a stand-in for the host’s frame, and content wider than the card fails the new critical dimension canvas-overflow-x. The criteria block is now asked in a report-only call of its own, so it never changes a score, and its cost is reported apart from the judgement’s.

2026-09-26 — v0.24.0: generate on your Claude login, and a card knows its one-shot action is spent

Section titled “2026-09-26 — v0.24.0: generate on your Claude login, and a card knows its one-shot action is spent”
  • An exact contract match is served without waiting for the installed-blueprints walk, which used to hold up an app’s first handshake after a server start. The walk still runs alongside it, and a marketplace-installed match, a miss and every similarity match wait for it, so an uninstalled interface is never served. createInstalledBlueprintsProvider takes an optional onWalk reporter that receives each walk’s scope, entries, evicted, listedRows and sweepMs, and a reporter that throws is reported as a new walk-report-threw issue kind.
  • A component can read whether its once-only action is already spent. useActionSpent(name) in @ggui-ai/wire is true when the card’s contract declares name oneShot and the action already fired, in this render or before a reload, so a consumed control can paint as submitted on first paint instead of looking live until a refused press. It flips when the card’s own dispatch commits and when a later frame carries a spend recorded elsewhere. This release declares the hook; generated components are taught it in the next, because a card imports @ggui-ai/wire through its runtime’s fixed export list and a name that list lacks fails at load. For hosts that build their own wiring, buildWireConfig now returns the config together with that actionSpent source and takes an optional spentInputsChanged notice.
  • ggui serve --local-cli-login is an opt-in way to generate without an API key: it uses the Claude login already on this machine. Your Claude plan’s terms and usage limits apply, and it is about 2x slower than an API key (2.2x at the median). It covers Anthropic routes only, a real Anthropic key always wins, and it is refused with --multi-user and --public-demo, because one person’s plan never serves other users. @ggui-ai/ui-gen exports CLAUDE_CODE_LOGIN_CREDENTIAL, the value a custom key resolver returns to select this path.
  • The handshake suggestion’s blueprintMeta declares an optional matchedIntent: the stored purpose of the saved interface a judged cache hit proposes, at most 280 characters, so an agent can compare it with its own intent before accepting, and re-handshake with forceCreate if it names a different task. Declared in this release; servers start sending it one release later, so a host that cached this release’s tool schema accepts it. The intent you send is stored with the interface and read again when later requests of this app are matched against it: describe the task, never the end user.
  • A handshake with forceCreate never proposes a saved interface. It skips the deployment pre-match and every find-similar probe, and goes straight to validating (and, if needed, repairing) the draft for a fresh build, which is what the paired ggui_render already did. Before, an agent that asked for a fresh build after an unwanted cache suggestion could be handed the same suggestion again.
  • handshake.decided telemetry no longer carries the decision’s reason text, which could quote the agent’s draft or name its contract’s fields. selectionReason is replaced by selectionReasonKind, one of curated-prematch, match-exact, match-semantic, verbatim, normalized, llm-repair, salvaged-subset, no-creds, negotiator-degraded, no-negotiator or unclassified, and selectionReasonHash, the first 16 hex characters of the reason’s SHA-256, so equal reasons still correlate. A TelemetrySink that read selectionReason must switch to the kind.
  • @ggui-ai/mcp-server-handlers/renders: a negotiator’s decision takes an optional reasonKind, typed by the newly exported HandshakeReasonKind, and every negotiator the package ships sets it. Breaking for direct callers of the decision builders: buildCacheReuseResult takes a required fourth argument, the tier that hit ('match-exact' or 'match-semantic'), and buildCreateFallback takes a required third argument, the fallback kind ('no-creds' or 'negotiator-degraded'), placed before the optional request variance, so a variance passed third moves to fourth.
  • ggui_runtime_pull’s tool_invoked log line carries the sessionId of a successful pull, so the first and last pull of a session bound how long its card stayed mounted. A pull that fails, and every other tool’s line, are unchanged.
  • A generator with the runtime probe wired and no evaluator runs the probe once after the coding turns (before, it did not run at all). That covers createUiGenerator({ enableRuntimeRender: true }) without an evaluation config, and a direct dispatchGeneration call without one, where the probe is wired by default. The probe never blocks a card: a recoverable render crash gets one repair turn (one more coding-model call) and one re-probe, then the generation serves what it has, and any other result serves as it is. The generation’s metadata reports the result in runtimeProbe and evalMs. runtimeProbe is a status-discriminated record: status: 'ran' carries a verdict ('pass', or 'fail' with the non-empty failChecks that failed, in the order the check kinds are declared); 'timed-out' / 'infra-skipped' / 'not-applicable' carry no verdict; every status that reached the check carries elapsedMs, and queuedMs when the check waited for a probe slot. When a repair turn was taken, repair is { attempted: true, compiled: false } (the pre-repair card is served and the top-level outcome is the probe that bought the turn) or { attempted: true, compiled: true, trigger } (the repaired card is served, the top-level outcome is its re-probe, and trigger is the probe that bought the turn). The bounds are the deployment’s inputs: createUiGenerator({ runtimeRenderProbe: { timeoutMs, heapMb, maxConcurrent } }) builds one check per generator, maxConcurrent caps that generator’s live probe workers (extra checks wait their turn in order), createRuntimeRenderCheck(config) builds a check directly, dispatchGeneration accepts one as runtimeRender, and warmupRuntimeRenderWorker() runs one real probe worker at boot and reports whether it could run, without throwing. Defaults unchanged: 30 s, 512 MB, no concurrency cap.
  • @ggui-ai/negotiator exports the rerank judge as a seam: RerankJudge, a function from a query and its candidates to a RerankDecision, and llmRerankJudge(llm), today’s LLM judge in that shape. MatchBlueprintDeps in @ggui-ai/mcp-server-handlers/renders takes an optional rerank: { judge, threshold } pair: the judge decides, its own threshold is the cut (judgeThreshold does not apply to it), and a pair wins over a bare llm. Without a pair, matching behaves exactly as before. Breaking for direct readers of the decision: RerankDecision.reason is now optional, because a judge may decide without prose, so code that reads it as a string needs a fallback.
  • The relay’s contract-violation answer is declared: ggui_runtime_submit_action can answer CONTRACT_VIOLATION with the violations that failed the card’s actionSpec, and @ggui-ai/protocol exports contractViolationSchema. Declared in this release; the relay starts enforcing it one release later, so a host that cached this release’s tool schema accepts the answer.
  • ggui_consume’s result declares an optional nextStep, a ggui_amend({ sessionId }) hint present when the drain returned events, so an agent answering a tap repaints the same card instead of rendering a new one. Declared in this release; servers start sending it one release later.
  • ggui_consume and ggui_runtime_submit_action log their session on the tool_invoked line, as ggui_runtime_pull does: sessionId plus counts and flags (eventCount, status, timeoutS and aborted on consume; consumerPresent on a committed dispatch), never gesture content.
  • onBlueprintResolution events carry their request context: optional appId, handshakeId and sessionId, plus proposedBlueprintId when the consumed handshake proposed a saved interface.
  • @ggui-ai/mcp-server exports buildLlmCaller, whose Anthropic calls take an optional { signal }, so a deployment that builds its own handshake negotiator can bound them with a timeout. Without a signal, calls run as before.
  • @ggui-ai/mcp-apps-react and @ggui-ai/mcp-apps-react-native describe themselves as MCP Apps host helpers in their npm description, keywords (mcp-apps), README and docs.

2026-09-25 — v0.23.0: a once-only action survives a reload, and ggui.json can clear what a deploy set

Section titled “2026-09-25 — v0.23.0: a once-only action survives a reload, and ggui.json can clear what a deploy set”
  • A spent once-only action stays spent after a reload. After a committed dispatch of a oneShot action, the built-in in-memory and SQLite session stores keep a record for that card, the ai.ggui/render slice lists the card’s spent names as spentOneShots, and the runtime seeds its guard from that record. On a re-served card, the first gesture on an action the card already spent is stopped and traced as one-shot-spent, exactly like a second gesture on a live card. The record counts only on the card it was written on, and only for names that card still declares oneShot; a card replaced by ggui_update starts fresh. The runtime stops the gesture, and how the control looks afterwards is still up to the component. Hosts that build their own wiring can pass getSpentOneShots to @ggui-ai/wire’s buildWireConfig. A custom GguiSessionStore can implement the new optional recordSpentOneShot. A store without it keeps today’s behaviour, and the server says so once at startup (spent_one_shots_not_durable). A malformed spentOneShots is dropped and reported to the host as spent-one-shots-invalid, never silently.
  • ggui.json accepts "theme": null and "generation": null. Locally they mean the same as leaving the field out: the shipped default tokens, and no generation route. On ggui deploy they clear the app’s stored theme or generation route, and the deploy prints theme cleared / generation cleared. Leaving a field out still leaves the stored value untouched.
  • @ggui-ai/protocol adds the write form of an app theme. appThemeWriteSchema admits null on the optional members outside the attestation (mode, name, frameless, fonts, imagery), and splitAppThemeWrite separates those clears from the document to store, so a write door can clear one member without clearing the theme. Attested members (cssVariables, keyframes) and required ones never clear by null.
  • A font face in the app’s stored theme that the card’s CSP blocks is reported to the host as font-face-blocked, the same event a blocked host-announced font raises; it used to fail with no event. A face’s origin is admitted when the card mounts, so a face added on a new origin while a card is open loads on that card’s next mount.
  • An expanded card (fit: 'fill') has one frame rhythm: a 16 px inset, first-level corners concentric with the panel’s, and a bleed prop for an element that should meet the panel’s edges. A hero band that opens the card bleeds on its own, so cards already served keep their band edge to edge.
  • The handshake’s semantic match returns to its own similarity floor, 0.2. Version 0.22.0 raised it to 0.3 to match ggui_search_blueprints, but the handshake scores a request’s intent alone against saved interfaces stored with their contract and intent. On that scale a real match (the same task, with the same contract) can score under 0.3, and 0.22.0 generated those fresh instead of reusing them. ggui_search_blueprints keeps 0.3. The two floors become one number again once the handshake scores a request the way it stores an interface.
  • A host that declares ui-message-turn in its Ggui-Host-Capabilities connection header, because it delivers a later gesture as the agent’s next turn, gets no nextStep on ggui_render, so the agent ends its turn when the card paints instead of long-polling ggui_consume. @ggui-ai/protocol exports the header name, the token and parseHostCapabilitiesHeader. A host that declares nothing, or only tokens the server does not know, sees no change.
  • The conformance kit grades the read-plane-only posture, where a server withholds the ai.ggui/render slice from tool results. A new catalog, runReadPlaneOnlyConformance from @ggui-ai/protocol-conformance/read-plane-only-conformance, checks that such a result carries the render locator on both structuredContent.resourceUri and _meta.ui.resourceUri, with the same value, that it carries no slice and no wsToken, and that reading the locator mounts the view. The posture now has its own SPEC section, §7.10.6. The host-helper catalog’s H2 check is stricter: a helper that advertises serverResources is now sent a resources/read, and refusing, dropping or re-shaping it fails where it used to pass. Helpers that advertise only serverTools, as both first-party helpers do, grade as before. SPEC §5.5.2 adds that emitters SHOULD send MISSING_META_GGUI_BOOTSTRAP, and that hosts SHOULD treat BOOTSTRAP_META_MISSING the same way.
  • The model registry’s lineup, the models a picker shows first, gains Claude Opus 5.5 and GPT-6 Luna. Claude Opus 5 becomes legacy and stays selectable. The default model, Claude Haiku 4.5, does not change.
  • ggui_ops_generate_blueprint returns the full 64-character codeHash, the same key ggui_ops_register_blueprint returns for the same code and the only form a code store accepts. It used to return a 32-character prefix, so its code could not be written to a guarded code store.
  • @ggui-ai/negotiator’s LLMCaller.callStructured returns Promise<unknown> and no longer takes a type parameter: an implementation returns the tool input as the model produced it, and the caller parses it, as the rerank judge and contract synthesis already did. An implementation written as callStructured<T>(…): Promise<T> still satisfies the interface. A call that passes a type argument no longer compiles; drop it and validate the result.
  • @ggui-ai/iframe-runtime exports every observability event type from its package root. OneShotUnenforceableEvent, new in 0.22.0, and ComponentEmptyEvent were reachable only from the ./observability subpath.
  • When the OAuth flow hands consent to an external consent page, that page is given only the name the client registered. For a client registered without a name, a client_name on the authorize link used to reach the page and show as the client’s self-declared name. It is now dropped, and the client reads as an unnamed client, as it does on the built-in consent page.
  • Generation: a runtime probe that runs out of time is reported as timed-out, with its elapsed time and the host’s load, never as a component crash. A slow host no longer tells the coding agent to fix a crash that did not happen. A patch the engine refuses before applying anything is reported to the coding agent as PATCH_INVALID and logged, instead of being counted as a failed self-check that never ran.

2026-09-23 — v0.22.0: a once-only action holds without a live session, and the self-hosted card never waits in silence

Section titled “2026-09-23 — v0.22.0: a once-only action holds without a live session, and the self-hosted card never waits in silence”
  • A once-only action now holds even when the card paints without a live session: past the session token’s lifetime, before the live channel connects, or on a host with no live channel. The ai.ggui/render slice carries the contract’s actionSpec whole, on every transport, and the runtime’s oneShot guard reads it from there. When a card has an action but no action spec reached it (for example, a render from an older server), its first dispatch posts one-shot-unenforceable to the host once. Before, the guard failed open without any signal. A spec the runtime can read only in part is reported, never dropped quietly: action-spec-invalid for a spec the write door would refuse, and action-spec-member-stripped for an entry member from a newer server. @ggui-ai/wire’s buildWireConfig takes an optional onActionSpecAbsent for hosts that build their own wiring.
  • The default MCP Apps shell a self-hosted server serves never waits in silence. If the host never sends the tool result, or never answers the view’s resources/read, the card shows a failure with Retry after 30 seconds and posts ggui:bootstrap-failed with MISSING_META_GGUI_BOOTSTRAP; a failed read’s cause rides in the message instead of being reported as MALFORMED_BOOTSTRAP. A host that never answers or refuses ui/initialize now gets UI_INITIALIZE_FAILED posted, not only a card. The reasons and the 30-second bound are the hosted runtime’s.
  • Blueprint reuse is stricter: the handshake’s semantic match now uses the same similarity floor as ggui_search_blueprints (0.3, was 0.2), so a short request that is the same task in fewer words may generate fresh instead of reusing a saved interface.
  • A saved interface whose intent is only a stand-in — an operator placeholder, an id, a manifest name, or a persona — is never proposed by semantic match; it is still reused when a request’s contract matches exactly. A row a persistent store kept from an earlier version carries no such mark until it is registered again. To keep an installed blueprint eligible, give its manifest a description; an operator registration keeps it with a seedPrompt.
  • Every generation names the build that made it: GenerationMetadata gains an optional build (GeneratorBuild in @ggui-ai/mcp-server-core — an optional package version, an engine-defined mode, and content digests), so results can be grouped by the engine build that produced them. @ggui-ai/ui-gen’s createUiGenerator fills it on every generation, as its precise UiGenBuild (the design mode, and digests of its prompt and boilerplate templates); createAdvancedUiGenerator and any generator that reports no build leave it absent.
  • Under the hood: an app theme written through ggui_ops_set_app_theme that carries overlay variables this server does not know, such as ones a newer @ggui-ai/design projects, is stored verbatim and the names are logged, instead of refused, so a newer caller can write to an older server during a rolling upgrade (a theme that leaves a known variable uncovered is still refused). In generation, a component that ignores a declared prop gets one bounded feedback round instead of shipping, a declared chat card is composed as a card for the chat bubble, eval calls’ prompt-cache tokens count toward tokens.total, a run the same-exchange guard ends says so on GenerationResult.sameExchangeBreak, and the Claude Code login child starts with an allowlisted environment instead of the parent’s, so a provider key or switch in the parent’s environment no longer reaches it.

2026-09-23 — v0.21.0: three new models, and the tool-call guards they need

Section titled “2026-09-23 — v0.21.0: three new models, and the tool-call guards they need”
  • New models: claude-opus-5-5, gpt-6-sol and gpt-6-luna can be selected by name; no default moves. Models that refuse a forced tool call — Claude Opus 5.5 and Claude Fable 5.1 — are no longer sent one, in generation or in the blueprint judge, and the gpt-6 generation sends the token-cap field the OpenAI API expects.
  • OAuth: the authorization response names its issuer (RFC 9207 iss, advertised in the server metadata), and client registration is rate-limited per IP (10 registrations per 10 minutes).
  • Sandboxed frames can preflight the public read routes — the runtime bundle, component code and session reads — and get the same open access the reads themselves always had.
  • Generation and reuse: the visual judge checks the card at the size it is declared to render, prompt-cache reads are reported for Google and OpenAI routes, and an action’s control shows its contract label verbatim. The blueprint judge now weighs only cached cards that clear the similarity floor (before, only the closest one had to), and GGUI_CACHE_TRACE=scores logs each reuse decision with its keys and scores but none of the request text.
  • A theme’s motion tempo now reaches the card. Every design-system transition reads --ggui-motion-duration-{fast,base,slow} and --ggui-motion-easing-{standard,emphasized,exit}, so a theme that states motion.duration / motion.easing animates the card at its own tempo; a theme that states none animates at the shipped tempo, as before. (Recorded late: this entry first went out without it.)

2026-09-19 — v0.20.0: a security fix to the OAuth registration door — upgrade if you run --oauth

Section titled “2026-09-19 — v0.20.0: a security fix to the OAuth registration door — upgrade if you run --oauth”
  • Security fix (ggui-ai/ggui @ggui-ai/mcp-server): the Dynamic Client Registration door now shape-checks every redirect_uris entry before storing it, bounds client_name, and the consent page names who is asking and where the browser will be sent. Before this release a registration could carry a malformed or unintended redirect target through the door. Self-hosters running ggui serve --oauth should upgrade to 0.20.0; the hosted service already has the fix (the hosted consent page named the client and destination from 2026-09-21).
  • The theme manifest is treated as a wire: a variable added to it in a newer release completes from what an older release’s theme already carries, so a theme composed on the previous version is not refused at the door — and the previous release’s manifest is pinned as the fixture that catches the next such growth.
  • Generation: form inputs are primed before the action-wiring probe runs, so a control that only enables once its field has a value is exercised rather than reported missing; the OpenAI adapters now report non-cached input and cache-read tokens separately, so a cost row reads what the provider actually billed.

2026-09-17 — v0.19.0: a spent action stays spent, and the theme names what the card cannot read

Section titled “2026-09-17 — v0.19.0: a spent action stays spent, and the theme names what the card cannot read”
  • A once-only action is spent for the life of the render, not just disabled on screen: a second gesture on a oneShot control is stopped by the runtime and traced as suppressed — it never reaches the agent and it is never dropped silently.
  • The theme says what it cannot do instead of skipping it: a document member the card’s projector cannot read produces a named diagnostic rather than the same output as if it were absent, the members no card paints yet say so on their own type, and corner radius is now projected by role — a host’s cards and its controls take two values, and the control primitives read theirs.
  • The stored theme can be read back in the shape a later write carries: beside the render-time read, a carry read returns exactly what was stored, so a host can reproduce a theme without re-deriving it.
  • Under the hood: a render names how it resolved its blueprint — what it looked for, whether it hit, and whether the registry deduplicated — an override.contract re-aims at its key the way override.variance already did, and the Google adapter hands its key to the model directly, so a stale GOOGLE_GENAI_API_KEY in the environment can no longer win over the key you configured.

2026-09-16 — v0.18.0: a contract can say an action fires once, and the card can be dismissed

Section titled “2026-09-16 — v0.18.0: a contract can say an action fires once, and the card can be dismissed”
  • An action that should happen once is now declared, not guessed: an action entry can carry oneShot, the generated card disables that control after it fires, and the agent is bound by the contract rather than by reading labels. Everything not marked oneShot stays repeatable — a control is never guarded by accident — and confirm remains an advisory signal for the author, not a gate.
  • The card can be dismissed by the host: a new host-bound ggui:dismiss intent forwards the user’s dismiss gesture to the host and never acts on it inside the card, so closing means whatever the host decides it means.
  • The doors are stricter and say why: a theme write that would destroy stored members it does not carry is refused, and the refusal names exactly which members would be lost (clearing is an explicit theme: null, never an omission); a stored contract or theme carrying a member from a later release is read with that member stripped and named, not silently dropped; and the generation profile has one read door for everything that reads it.
  • The host’s type scale and rhythm now reach the card: typeScale moves the type scale and rhythm.base re-derives the spacing — carried on the wire since 0.17.0, painted from this release.

2026-09-15 — v0.17.0: a theme can name its typeface and imagery, and the hosted card loads them

Section titled “2026-09-15 — v0.17.0: a theme can name its typeface and imagery, and the hosted card loads them”
  • A theme declares its own typeface and imagery: typography.faces in the document (https: sources) rides the wire as fonts, alongside imagery slots for a brand mark, a hero image and a repeating pattern. The hosted card admits every declared origin in its content-security policy and inlines the @font-face rules in the page it serves, so a named family actually loads — and ggui deploy now carries the faces the document declares instead of dropping them.
  • Declared but not yet painted: the type-role scale, the bounded motion tempo, and the scrim overlay are carried on the wire and validated at the door, but no card wears them yet — the work that makes them visible is still in flight. Nothing breaks if you declare them today; nothing changes on screen either. The motion tempo became visible in v0.21.0.
  • A read door never drops a theme in silence. A theme this release refuses is reported to the embedding host with the reasons, and a theme carrying members a NEWER writer sent is kept, stripped, and reported with the member names — so a rolling deployment can see exactly what an older card could not read.
  • A card the host shows fullscreen fills the frame it was given: the frameless silhouette is a fit rule now, the fill is anchored on the frame rather than a parent, and a mounted list no longer inherits the browser’s bullets and indent.
  • A long-held poll ends the moment its caller goes away, and a relay error counts toward backing off — a card whose host times out its own calls no longer re-fires held requests forever.
  • For anyone running the evaluators: the visual judge names the design tree it painted with, a terminal action the static probe cannot reach reads as not-measured rather than as a failure, and an evaluation round that throws reports the failure as its result instead of returning nothing.

2026-09-10 — v0.16.0: theming revised — layering roles, one anchor per family, the app theme on the wire

Section titled “2026-09-10 — v0.16.0: theming revised — layering roles, one anchor per family, the app theme on the wire”
  • Theming is revised (draft-2026-09-10): a theme.json states the six layering roles — ground/onGround, container/onContainer, sunken/onSunken — and one 500 anchor per colour family; the ramps, inks, containers, outlines and the type scale are derived. font.size, font.lineHeight, motion.duration, motion.easing and $metadata.fontUrl are refused, and ggui serve refuses to start on a theme file in the old shape — ggui theme validate <path> names the field.
  • An app’s theme on the wire is its projection for both modes: { overlays: { light, dark }, overlayHash, mode?, name? }. The embedding host owns the runtime mode — its announced mode wins and the app’s mode is a default — and a theme the write door refuses answers invalid_app_config with one of four bodies (uncovered, unknown, overlayHash: "mismatch", refused: "v1 shape").
  • Fonts are declared as typography.faces (https: sources only); ggui deploy names families the hosted path does not deliver yet.
  • The runtime theme-registration tools (ggui_ops_register_theme / ggui_ops_list_themes / ggui_ops_delete_theme) are gone.
  • ggui serve stops cleanly on Ctrl-C / SIGTERM: exit code 0, nothing native on stderr. Before, with the local embedding model loaded, every stop ended in a libc++abi … mutex lock failed abort (SIGABRT). The boot log now also says when the model is ready ([ggui:embedding] warm — local model loaded in …ms), so a supervisor can tell semantic search is live. The drain is bounded: if a handle ever keeps the process alive past 5 s after shutdown, ggui serve names it on stderr and exits with the same code instead of hanging.
  • ggui serve --multi-tenant is now ggui serve --multi-user. The flag turns on per-user LLM-key management at /settings (each authenticated user manages their own keys; builder identities are rejected at that gate) — the old name described a party this server does not have. No alias: the old flag is refused as unknown.
  • The endpoint-level refusal’s data.refusal carries appId — the app the refused endpoint serves, equal to the endpoint path’s {appId} — so a repair loop keys on it instead of parsing the message. A server built against 0.15.0 sends the refusal without it.
  • UNAUTHORIZED moves from -32001 to -32007: -32001 is the MCP SDK client’s own RequestTimeout, minted locally — a client reading the number could not tell a server’s refusal from its own timeout, the class the -32000 move closed. -32001 is retired-reserved.
  • Two refusal codes lose the word “tier”: model_not_in_tier → model_not_allowed (the caller picks a model the app is allowed to use — the one refusal an agent may act on itself) and already_on_tier → subscription_unchanged (the subscription requested is the one the app already holds). A server built against 0.15.0 still emits the old names; the 0.16.0 conformance kit refuses them.
  • A PendingEventConsumer adapter appends and drains the protocol’s PendingEvent — {id, envelope, createdAt}, the envelope always the entry object; the sequence field, which nothing ever wrote or read, is gone. Every row is validated through the new pendingEventSchema at the store boundary: append refuses a malformed row before storing it (PendingEventMalformedError), and a drain never returns a row that fails the schema nor drops a well-formed sibling because one failed — the sqlite adapter refuses the drain whole and rolls back, a DynamoDB-style adapter quarantines the row with a pending_event_malformed log and delivers the rest, and ggui_consume reports a refused drain as a typed failure carrying {events: [], status} rather than a raw error. The bytes of a well-formed row are unchanged.
  • The server-level instructions and the tool descriptions now say the same thing, and the true thing, about the render response: a refusal is five keys ({code, message, fix, retry, handshake: 'intact'}); the retry rule follows the refusal’s retry class (after-fix is yours only when fixBy is caller; later retries after its delay; next-period and never are not retried); a missing handshakeId is an input-validation error (-32602) while an unknown one fails as not found; a rendered or failed render consumes the handshake and a refused render or a recoverable validation error leaves it intact; a consume event carries seven keys (type, sessionId, and the five gesture fields); ggui_consume teaches end-your-turn on an empty drain instead of “exit only when expired”; the never-emitted missing_props code is gone from both, and ggui_list_sessions is described by the hostName + hostSessionId pair it actually takes. A contract test pins every one of these against the booted server’s tools/list.
  • @ggui-ai/wire’s connection state splits into a read view and a claimed writer: the root barrel exports connectionSource ({subscribe, getSnapshot}, what useRender().isConnected reads) and nothing that writes; connectionStore / ConnectionStore are gone. The writer lives on @ggui-ai/wire/internal — claimConnectionWriter(name) — and a deployment’s runtime claims it once, eagerly at boot; a second claim throws ConnectionWriterConflictError naming both parties — the store is one per document (anchored on globalThis under Symbol.for('ai.ggui.wire/connection-store'), shared by every copy of the module), so the same name twice really is two copies of the runtime in one document, and a foreign value at that slot is refused with ConnectionStoreSlotError rather than adopted or overwritten; a released writer that writes throws ConnectionWriterReleasedError. Generated component code cannot reach the writer: it is not on the barrel, the import rewriter refuses @ggui-ai/wire/internal at load time, and the generation gate refuses it before any code is emitted — one forbidden list, three doors.
  • A render denied by the per-app render-rate cap is a refusal like any other render-gate refusal — outcome: 'refused', refusal.code: 'app_rate_limited', retry: 'later' carrying the wait, handshake intact — instead of a thrown error whose text lost the code, the fix and the retry class.
  • RateLimitedError is gone from @ggui-ai/mcp-server-core: a limiter denial has no thrown form — the render handler returns the registry’s app_rate_limited refusal; the RateLimiter seam (RateLimitCheckInput, RateLimitDecision) stays. Hosted GGUI’s RateLimitedError → 429 arm went with it, so no first-party server emits -32013 any more.
  • A rate-limit decision can now say whose cap bound: RateLimitDecision.scope is 'app' (the default when absent — existing limiters are unchanged) or 'issuer', and the render gate’s refusal follows it — an issuer-cap denial is issuer_rate_limited and names the issuing identity, where before every denial said the app was over its cap.
  • All 32 published @ggui-ai/* packages moved to 0.16.0 in lockstep.

2026-09-05 — v0.15.0: declarations that resolve everywhere, and a browser entry

Section titled “2026-09-05 — v0.15.0: declarations that resolve everywhere, and a browser entry”
  • Every published type declaration now carries explicit .js extensions on its relative imports, so @ggui-ai/* typechecks under TypeScript’s NodeNext / Node16 module resolution as well as bundler. Before this, a NodeNext project saw no exported member on names such as PLATFORM_ERROR_CODES.
  • @ggui-ai/protocol/wire is a browser entry: the same names, only the modules a browser validates. The iframe runtime and the React / React Native host helpers import it; the iframe bundle is 37 KB smaller.
  • Authorization refusals on the MCP endpoints answer JSON-RPC -32001 (UNAUTHORIZED) instead of the generic -32000, so a client can tell a refusal from a dropped connection. A deployment’s error mapper may attach JSON-RPC data — a structured reason — to such a refusal.
  • ggui_consume, ggui_list_sessions and ggui_emit declare their wire shapes from the protocol; ggui_emit now advertises the seq it always returned.
  • The per-app MCP endpoint answers GET and DELETE with 405 + Allow: POST instead of a text/html 404, so an optional SSE listener (Google ADK’s) stops logging Not Found every turn.
  • If a host refuses the iframe’s ui/initialize, the iframe runtime now records the boot as failed and keeps its relay closed, instead of forwarding gestures from a document that never connected.
  • All 32 published @ggui-ai/* packages moved to 0.15.0 in lockstep.

2026-09-05 — v0.14.0: a refusal is an outcome, and Node 22 is the floor

Section titled “2026-09-05 — v0.14.0: a refusal is an outcome, and Node 22 is the floor”
  • A render can now be refused before anything is generated, and the refusal is its own outcome — not a failure dressed as one. ggui.json declares protocol draft-2026-09-04; the loader refuses a manifest whose stamp it does not support with UPGRADE_REQUIRED, and a dated migration doc walks the change.
  • ggui_render accepts a model route in either wire form (provider:model or provider/model), and LiteLLM aliases resolve in both.
  • Handlers are built with defineHandler — a handler’s output type derives from its schema, so a result that omits or invents a key fails to compile rather than being silently stripped on the wire. Failed calls can now be observed with postFailureHook.
  • Rich text renders GFM pipe tables as tables.
  • Every @ggui-ai/* package now requires Node 22 or newer.
  • All 32 published @ggui-ai/* packages moved to 0.14.0 in lockstep.

2026-08-31 — v0.13.0: calm chrome for launch day

Section titled “2026-08-31 — v0.13.0: calm chrome for launch day”
  • The pre-render loading surface is neutral and scheme-aware now — a light ground in light mode, a dark ground in dark mode — instead of the fixed dark slab that flashed while components rendered. Themes still repaint it the moment they load, and embedders keep a one-variable override.
  • A branded working-state mark plays while a component renders: GGUI’s four glyphs assemble, swap inks, and clear on a calm cycle. Serving deployments can replace it with their own mark (or disable it) via the shell assembler’s new loadingIndicator option; it honors reduced-motion and retires the instant real content paints.
  • The generated-component feedback row is rebuilt and off by default: two theme-aware thumb icons (up / down) replace the old text buttons, and the row renders only for deployments that opt in via the runtime’s uiFeedback boot option.
  • Package builds publish atomically — a dependency’s compiled output is never observably half-present to concurrent tooling.
  • All 32 published @ggui-ai/* packages moved to 0.13.0 in lockstep.

2026-08-24 — v0.12.0: the server tells the truth about itself

Section titled “2026-08-24 — v0.12.0: the server tells the truth about itself”
  • The MCP serverInfo, the MCP Apps appInfo, and /ggui/health now all report the real shipped release instead of placeholder versions — every connected agent sees which build it is actually talking to, and a new runtime constant keeps that identity locked to the published packages.
  • A registered theme now travels with the render: the server delivers the theme’s full light/dark token ladders alongside the overlay, and the runtime installs the delivered ladder as the base — a registered theme looks the same everywhere, with no compiled-in copy to drift.
  • Theme registration gained guardrails: a coverage validator proves a theme defines every token the design system consumes, unknown theme ids are refused where they were typed, and a brand-shaped overlay gets a pointer to full registration instead of silently painting on defaults.
  • Solid-accent components (buttons, active tabs, checkbox marks) read their text color from the theme’s on-accent tokens instead of hardcoded white, with a WCAG AA contrast sweep over every producible pairing now standing in the test suite.
  • Generated UIs are checked for prop sensitivity — a displayed value that ignores its prop is caught before it ships.
  • All 32 published @ggui-ai/* packages moved to 0.12.0 in lockstep.

2026-08-21 — v0.11.0: rendered cards match the app around them

Section titled “2026-08-21 — v0.11.0: rendered cards match the app around them”
  • Host-announced color palettes now reach generated UI: the spec’s --color-* variables translate onto GGUI’s design tokens and layer beneath the app’s own theme — the app’s theme always wins, the host fills what it didn’t set.
  • A per-app theme now selects the full base token set, not just an overlay: its color mode picks the right light/dark ladder, and a theme whose name matches a registered preset gets that preset as its base. Themes can also declare themselves frameless when the embedding app draws the card’s outline itself.
  • The React Native host view no longer paints its own border, and it reports content-size changes through a new onSizeChanged callback so embedders can auto-size the card.
  • Tool-result listeners now register before the host handshake on every boot path, closing a timing gap on re-emitted results.
  • All 32 published @ggui-ai/* packages moved to 0.11.0 in lockstep.

2026-08-19 — v0.10.0: renders validate against the schema the handshake promised

Section titled “2026-08-19 — v0.10.0: renders validate against the schema the handshake promised”
  • The handshake now returns the enforced props schema for a blueprint — plus its hash and profile — and every render is validated against that exact schema before it ships. What the agent was promised is what the view receives.
  • New protocol helpers for working with props schemas: build the enforced schema, canonicalize it to stable bytes, classify its profile, and validate data against it. Servers get a dedicated props-schema-hash entry point for cache keys.
  • The conformance kit gains a props-schema catalog — seven polyglot cases any implementation can run to prove its schema handling matches the spec.
  • All 32 published @ggui-ai/* packages moved to 0.10.0 in lockstep.

2026-08-13 — v0.9.0: host helpers get their real name

Section titled “2026-08-13 — v0.9.0: host helpers get their real name”
  • Breaking: @ggui-ai/react is now @ggui-ai/mcp-apps-react, and @ggui-ai/react-native is now @ggui-ai/mcp-apps-react-native. Update your imports — the old package names are retired. The new names say what these packages are: MCP Apps host-helper libraries, not a GGUI SDK.
  • GguiRender and useWebSocket are removed from both packages. Rendering semantics live in the mounted view (iframe runtime), never in the helper.
  • First release of the React Native host helper: @ggui-ai/mcp-apps-react-native, with <McpAppIframe> as its front door.
  • All 32 published @ggui-ai/* packages moved to 0.9.0 in lockstep.

2026-08-11 — hub.ggui.ai: browse the public registry

Section titled “2026-08-11 — hub.ggui.ai: browse the public registry”
  • The registry browser is live at hub.ggui.ai — search published blueprints and gadgets, filter by the MCP tool or server they bind to, and see works-with chips and verified-publisher badges at a glance.

2026-08-10 — v0.7.0: renders that outlive their cache

Section titled “2026-08-10 — v0.7.0: renders that outlive their cache”
  • Reopening an old card from chat history now works even after its render was evicted from cache: resources/read re-mints the view from its stored blueprint and committed body, so old cards rehydrate instead of going stale.
  • Resource-read failures are typed against a closed error enum, so a host can tell “gone” from “denied” from “malformed” instead of guessing.
  • Hosted renders are kept indefinitely by default.
  • All @ggui-ai/* packages moved to 0.7.0 in lockstep.
  • The iframe runtime now reads the host’s advertised capabilities at boot, instead of assuming every embedding host behaves the same way.
  • Hosts that don’t relay ui/message back to the agent get an honest, one-time explanation in the UI instead of a gesture that silently goes nowhere.
  • Relay incapability is only latched after a confirmed failure — a host is never assumed broken just because it hasn’t advertised a capability yet.

2026-08-08 — Browser CORS support on the MCP plane

Section titled “2026-08-08 — Browser CORS support on the MCP plane”
  • The MCP server now mounts a browser CORS layer with an allowlisted-origins model, so browser-based MCP clients can call it directly without a proxy.
  • Origin and Host validation is enforced on both the HTTP and WebSocket upgrade ingresses, closing a DNS-rebinding gap.
  • The CLI gained a --browser-origin flag (and GGUI_BROWSER_ORIGINS environment variable) to configure which origins are allowed.
  • The /ggui/health endpoint now surfaces the effective allowed origins for debugging.
  • UI components generated by GGUI can now carry rich-text descriptions — bold, italics, links, and inline code — instead of plain strings only.
  • The rich-text parser is now vendored directly in the design package rather than pulled in as an external dependency.
  • All @ggui-ai/* packages moved to 0.6.3 in lockstep.

The current protocol draft is draft-2026-09-10. See Version policy for how draft versions relate to semver on @ggui-ai/protocol, and what changes would trigger the next draft bump.