@ggui-ai/mcp-server-handlers 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/ops-apps/delete-app.d.ts +26 -9
  2. package/dist/ops-apps/delete-app.d.ts.map +1 -1
  3. package/dist/ops-apps/delete-app.js +34 -11
  4. package/dist/ops-apps/index.d.ts +1 -1
  5. package/dist/ops-apps/index.d.ts.map +1 -1
  6. package/dist/ops-apps/index.js +1 -1
  7. package/dist/ops-apps/types.d.ts +32 -1
  8. package/dist/ops-apps/types.d.ts.map +1 -1
  9. package/dist/ops-apps/types.js +16 -0
  10. package/dist/ops-blueprint/generate.d.ts.map +1 -1
  11. package/dist/ops-blueprint/generate.js +6 -2
  12. package/dist/ops-blueprint/register.d.ts.map +1 -1
  13. package/dist/ops-blueprint/register.js +7 -2
  14. package/dist/ops-connector-keys/types.d.ts +7 -0
  15. package/dist/ops-connector-keys/types.d.ts.map +1 -1
  16. package/dist/renders/blueprint-durability.d.ts +111 -0
  17. package/dist/renders/blueprint-durability.d.ts.map +1 -0
  18. package/dist/renders/blueprint-durability.js +85 -0
  19. package/dist/renders/blueprint-registry.d.ts +24 -0
  20. package/dist/renders/blueprint-registry.d.ts.map +1 -1
  21. package/dist/renders/blueprint-registry.js +9 -0
  22. package/dist/renders/code-delivery-events.d.ts +76 -0
  23. package/dist/renders/code-delivery-events.d.ts.map +1 -0
  24. package/dist/renders/code-delivery-events.js +60 -0
  25. package/dist/renders/fetch-gadget-types.d.ts +7 -3
  26. package/dist/renders/fetch-gadget-types.d.ts.map +1 -1
  27. package/dist/renders/fetch-gadget-types.js +7 -3
  28. package/dist/renders/generation-cache.d.ts +9 -0
  29. package/dist/renders/generation-cache.d.ts.map +1 -1
  30. package/dist/renders/handshake.d.ts +5 -3
  31. package/dist/renders/handshake.d.ts.map +1 -1
  32. package/dist/renders/handshake.js +9 -4
  33. package/dist/renders/index.d.ts +3 -0
  34. package/dist/renders/index.d.ts.map +1 -1
  35. package/dist/renders/index.js +13 -0
  36. package/dist/renders/render-identity.d.ts +178 -0
  37. package/dist/renders/render-identity.d.ts.map +1 -0
  38. package/dist/renders/render-identity.js +136 -0
  39. package/dist/renders/render.d.ts +39 -5
  40. package/dist/renders/render.d.ts.map +1 -1
  41. package/dist/renders/render.js +198 -83
  42. package/dist/renders/sync-context.d.ts +13 -1
  43. package/dist/renders/sync-context.d.ts.map +1 -1
  44. package/dist/renders/sync-context.js +6 -1
  45. package/dist/renders/update.d.ts +15 -1
  46. package/dist/renders/update.d.ts.map +1 -1
  47. package/dist/renders/update.js +6 -1
  48. package/package.json +7 -7
@@ -53,6 +53,8 @@ import { handlerFailure, isHandlerFailure, } from '../types.js';
53
53
  import { consumeHandshakeRecord, peekHandshakeRecord, HandshakeNotFoundError, } from './handshake.js';
54
54
  import { evaluateProvisionalPreviewGate, finalizeProvisionalPreview, kickoffProvisionalPreview, } from './provisional-preview.js';
55
55
  import { assertNoDuplicateGadgetHooks } from './assert-no-duplicate-gadget-hooks.js';
56
+ import { reportRenderCodeWriteFailed } from './code-delivery-events.js';
57
+ import { writeRenderIdentity } from './render-identity.js';
56
58
  import { findBlueprintExact, readBlueprintById, registerBlueprint, } from './blueprint-registry.js';
57
59
  import { assertGadgetsRegistered, filterDescriptorsToContract, } from './assert-gadgets.js';
58
60
  import { fetchGadgetTypes } from './fetch-gadget-types.js';
@@ -316,6 +318,24 @@ export function createGguiRenderHandler(deps) {
316
318
  // the §6 re-resolution and the cold-gen registration below both
317
319
  // key on it, so reuse / registration stay on the same identity.
318
320
  const effectiveVariantKey = variantKey(effectiveVariance);
321
+ // Contract axis of that same reuse key. Pure over the effective
322
+ // contract, so it is computed ONCE here and read everywhere:
323
+ // the §6 exact re-resolution, the wire `contractHash` field, the
324
+ // resource-URI segment, and the durable identity record every
325
+ // commit site writes. Hoisted to handler scope because the
326
+ // identity write-through happens at commit time — well before
327
+ // the wire output is assembled.
328
+ const effectiveContractKey = blueprintKey(effectiveContract);
329
+ // Bind the identity slice a commit will record. The blueprint id
330
+ // is per-path (a reuse knows it up front; a cold gen resolves it
331
+ // via registration immediately BEFORE its success commit — #460),
332
+ // which is why it is a parameter and the contract/variant axes
333
+ // are captured.
334
+ const identityWriterFor = (blueprintId) => (committed) => writeRenderIdentity(deps.renderIdentityStore, committed, {
335
+ blueprintId,
336
+ contractKey: effectiveContractKey,
337
+ variantKey: effectiveVariantKey,
338
+ });
319
339
  // Resolved gadget catalog, lifted to handler scope. When
320
340
  // `appMetadataStore` is bound, the registry-membership block
321
341
  // below captures the catalog (App record's `gadgets`, or
@@ -606,16 +626,20 @@ export function createGguiRenderHandler(deps) {
606
626
  contentType: 'application/javascript+react',
607
627
  createdAt: nowEpochMs,
608
628
  lastActivityAt: nowEpochMs,
609
- expiresAt: nowEpochMs + DEFAULT_RENDER_TTL_MS,
629
+ expiresAt: nowEpochMs + (deps.renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
610
630
  eventSequence: 0,
611
631
  };
612
632
  try {
613
- await deps.renderStore.commit({
633
+ const committed = await deps.renderStore.commit({
614
634
  render: placeholder,
615
635
  appId: ctx.appId,
616
636
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
617
637
  });
618
638
  placeholderCommitted = true;
639
+ // The placeholder is a real row a locator can address, so it
640
+ // gets a record too — the later in-place replacement
641
+ // overwrites it with the settled identity.
642
+ await identityWriterFor(null)(committed);
619
643
  }
620
644
  catch {
621
645
  // Defensive — a placeholder-commit failure is not fatal to
@@ -671,18 +695,19 @@ export function createGguiRenderHandler(deps) {
671
695
  kind: 'mcp-apps-probe',
672
696
  createdAt: nowEpochMs,
673
697
  lastActivityAt: nowEpochMs,
674
- expiresAt: nowEpochMs + DEFAULT_RENDER_TTL_MS,
698
+ expiresAt: nowEpochMs + (deps.renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
675
699
  eventSequence: 0,
676
700
  props: { intent: story.intent },
677
701
  };
678
702
  try {
679
- await deps.renderStore.commit({
703
+ const committed = await deps.renderStore.commit({
680
704
  render: probeRender,
681
705
  appId: ctx.appId,
682
706
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
683
707
  });
684
708
  safelyNotifyGguiSessionCommit(deps.channelNotifier, sessionId, probeRender);
685
709
  generatedCodeReady = true;
710
+ await identityWriterFor(null)(committed);
686
711
  }
687
712
  catch {
688
713
  // Commit failure leaves codeReady=false; downstream synth
@@ -796,7 +821,7 @@ export function createGguiRenderHandler(deps) {
796
821
  // `(contractKey, effectiveVariantKey)` and reuse a stored
797
822
  // component for that exact variant if one exists (per-app first,
798
823
  // then seed pools — see fan-out comment above).
799
- const bp = await findExactAcrossPools(blueprintKey(effectiveContract), effectiveVariantKey);
824
+ const bp = await findExactAcrossPools(effectiveContractKey, effectiveVariantKey);
800
825
  if (bp) {
801
826
  blueprintHit = {
802
827
  id: bp.id,
@@ -808,11 +833,18 @@ export function createGguiRenderHandler(deps) {
808
833
  }
809
834
  }
810
835
  if (blueprintHit) {
811
- generatedCodeReady = await commitCachedGguiSession(deps.renderStore, deps.provisionalPreview, deps.channelNotifier, deps.checkRenderContracts, {
836
+ // Reuse → the stored UUID is the materialised component id.
837
+ // `cache.cachedBlueprintId === blueprintId` on a hit (§9.3).
838
+ // Resolved BEFORE the commit so the identity record written
839
+ // at that commit already carries the id (no backfill needed
840
+ // on this path).
841
+ resolvedBlueprintId = blueprintHit.id;
842
+ generatedCodeReady = await commitCachedGguiSession(deps.renderStore, deps.provisionalPreview, deps.channelNotifier, deps.checkRenderContracts, deps.renderTtlMs, {
812
843
  sessionId,
813
844
  appId: ctx.appId,
814
845
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
815
846
  story,
847
+ writeIdentity: identityWriterFor(blueprintHit.id),
816
848
  cacheHit: {
817
849
  cachedBlueprintId: blueprintHit.id,
818
850
  similarity: blueprintHit.cosine,
@@ -874,9 +906,6 @@ export function createGguiRenderHandler(deps) {
874
906
  kind: 'full-template',
875
907
  reason: `full-template: reused stored blueprint ${blueprintHit.id}; 1 generation call avoided`,
876
908
  };
877
- // Reuse → the stored UUID is the materialised component id.
878
- // `cache.cachedBlueprintId === blueprintId` on a hit (§9.3).
879
- resolvedBlueprintId = blueprintHit.id;
880
909
  }
881
910
  else {
882
911
  // The `.d.ts` fetch is deferred to HERE — the cold-gen
@@ -890,10 +919,57 @@ export function createGguiRenderHandler(deps) {
890
919
  const resolvedGadgetTypes = resolvedAppLibraries !== undefined
891
920
  ? await fetchGadgetTypes(resolvedAppLibraries)
892
921
  : undefined;
893
- const outcome = await runGenerationIntoGguiSession(deps.generation, deps.renderStore, deps.provisionalPreview, deps.channelNotifier, deps.checkRenderContracts, deps.generator, {
922
+ const generationCache = deps.generation.cache;
923
+ const outcome = await runGenerationIntoGguiSession(deps.generation, deps.renderStore, deps.provisionalPreview, deps.channelNotifier, deps.checkRenderContracts, deps.generator, deps.renderTtlMs, {
894
924
  ctx,
895
925
  sessionId,
896
926
  story,
927
+ // #460 — the callee binds each commit's id itself:
928
+ // failure commits bind null; the success commit binds
929
+ // whatever `resolveBlueprintId` returned, resolved
930
+ // BEFORE that commit (no backfill exists anymore).
931
+ writeIdentityFor: identityWriterFor,
932
+ // Registration is the resolve hook, passed only when a
933
+ // cache is bound (unbound deployments keep null ids —
934
+ // #445). Failures swallow to undefined inside
935
+ // `safelyRegisterBlueprint`, incl. fail-closed
936
+ // rejections. The cache handle is captured as a const so
937
+ // the async closure keeps the narrowing.
938
+ ...(generationCache
939
+ ? {
940
+ resolveBlueprintId: async (produced) => safelyRegisterBlueprint({
941
+ embedding: generationCache.embedding,
942
+ vectorStore: generationCache.vectorStore,
943
+ index: generationCache.index,
944
+ // Forwarded so a fresh mint is written
945
+ // through to durable storage. Undefined on
946
+ // deployments that bound none, which
947
+ // `registerBlueprint` treats as a no-op.
948
+ ...(generationCache.durability !== undefined
949
+ ? { durability: generationCache.durability }
950
+ : {}),
951
+ }, ctx.appId, {
952
+ kind: 'template',
953
+ contract: story.contract,
954
+ intent,
955
+ componentCode: produced.componentCode,
956
+ // Engine provenance from the generator's own
957
+ // metadata stamp — a fresh generation mints
958
+ // an `llm`-sourced row.
959
+ source: produced.source,
960
+ // Register under the EFFECTIVE variance
961
+ // (proposed on accept, re-aimed on
962
+ // `override.variance`) so the row's
963
+ // `variantKey` equals `effectiveVariantKey`
964
+ // — the same `(contractKey, variantKey)`
965
+ // identity the §6 re-resolution and the wire
966
+ // output key on.
967
+ ...(effectiveVariance !== undefined
968
+ ? { variance: effectiveVariance }
969
+ : {}),
970
+ }),
971
+ }
972
+ : {}),
897
973
  ...(runtimeProps !== undefined
898
974
  ? { runtimeProps: runtimeProps }
899
975
  : {}),
@@ -925,36 +1001,12 @@ export function createGguiRenderHandler(deps) {
925
1001
  ? 'cold: generated fresh — no stored component was reused for this render'
926
1002
  : 'cold: generation failed — no stored component was produced or reused',
927
1003
  };
928
- // Register the produced blueprint into the registry so
929
- // future calls can hit Tier 1 (exact contract match) or
930
- // Tier 2 (semantic neighbour). The minted UUID becomes this
931
- // render's `blueprintId` (a fresh generation mints a new id).
932
- // Register under the EFFECTIVE variance (proposed on accept,
933
- // re-aimed on `override.variance`) so the row's `variantKey`
934
- // equals `effectiveVariantKey` — the same
935
- // `(contractKey, variantKey)` identity the §6 re-resolution
936
- // and the wire output key on. Never the default sentinel when
937
- // an override re-aimed the variant.
938
- if (outcome.ok && outcome.componentCode && outcome.source) {
939
- const registered = await safelyRegisterBlueprint({
940
- embedding: deps.generation.cache.embedding,
941
- vectorStore: deps.generation.cache.vectorStore,
942
- index: deps.generation.cache.index,
943
- }, ctx.appId, {
944
- kind: 'template',
945
- contract: story.contract,
946
- intent,
947
- componentCode: outcome.componentCode,
948
- // Engine provenance travels on the outcome — the
949
- // generator stamped its own slug + the resolved
950
- // model id into metadata, and a fresh generation
951
- // mints an `llm`-sourced row.
952
- source: outcome.source,
953
- ...(effectiveVariance !== undefined
954
- ? { variance: effectiveVariance }
955
- : {}),
956
- });
957
- resolvedBlueprintId = registered;
1004
+ // #460 — registration already ran INSIDE the generation
1005
+ // call, before the success commit (the resolve hook
1006
+ // above); the outcome carries the id the committed
1007
+ // identity record was written with. No backfill exists.
1008
+ if (outcome.ok) {
1009
+ resolvedBlueprintId = outcome.blueprintId;
958
1010
  }
959
1011
  }
960
1012
  }
@@ -990,15 +1042,16 @@ export function createGguiRenderHandler(deps) {
990
1042
  contentType: 'application/javascript+react',
991
1043
  createdAt: nowEpochMs,
992
1044
  lastActivityAt: nowEpochMs,
993
- expiresAt: nowEpochMs + DEFAULT_RENDER_TTL_MS,
1045
+ expiresAt: nowEpochMs + (deps.renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
994
1046
  eventSequence: 0,
995
1047
  };
996
1048
  try {
997
- await deps.renderStore.commit({
1049
+ const committed = await deps.renderStore.commit({
998
1050
  render: placeholder,
999
1051
  appId: ctx.appId,
1000
1052
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1001
1053
  });
1054
+ await identityWriterFor(null)(committed);
1002
1055
  }
1003
1056
  catch {
1004
1057
  // Defensive, matching the provisional-preview placeholder
@@ -1015,10 +1068,30 @@ export function createGguiRenderHandler(deps) {
1015
1068
  // several branches above (cache-hit, fresh generation, MCP Apps
1016
1069
  // inbound) — re-reading is simpler than threading a reference
1017
1070
  // through every branch and matches resultMeta's own pattern.
1018
- // Failures are silent: on a put error or a missing render we
1019
- // fall through with no codeUrl. Without codeUrl, the iframe
1020
- // falls back to live-mode (wsUrl+token) — the render is
1021
- // delivered via the live-channel WS subscribe.
1071
+ //
1072
+ // A store failure does NOT fail the render: the code is already
1073
+ // generated and the row already committed. But nothing catches
1074
+ // the envelope either — the guard above narrowed this to a
1075
+ // compiled-component render, and `codeUrl` is the only STATIC
1076
+ // delivery surface such a render has (the other static mode,
1077
+ // system-card `kind`, is excluded by that same guard). What
1078
+ // survives is deployment-shaped, and no arm of it is visible on
1079
+ // the wire:
1080
+ //
1081
+ // - `mintWsToken` wired ⇒ the slice still carries the live
1082
+ // trio, the iframe subscribes, and the WS delivers the
1083
+ // render body. Cost: the zero-round-trip first paint.
1084
+ // - A host that resolves `resourceUri` re-mints `codeUrl`
1085
+ // against this same store at READ time, so a transient
1086
+ // fault costs it nothing.
1087
+ // - Neither ⇒ this envelope's slice keeps `runtimeUrl` and no
1088
+ // mode discriminator, which hosts read as "not a mountable
1089
+ // ggui render".
1090
+ //
1091
+ // Hence the named event rather than a swallow — see
1092
+ // `code-delivery-events.ts`. A render with no `componentCode`
1093
+ // (the placeholder / system arms) is not a failure and emits
1094
+ // nothing: there was never a body to deliver.
1022
1095
  let codeUrl;
1023
1096
  let codeHash;
1024
1097
  if (deps.codeStore && deps.codeBaseUrl) {
@@ -1037,8 +1110,17 @@ export function createGguiRenderHandler(deps) {
1037
1110
  codeUrl = `${base}/code/${hash}.js`;
1038
1111
  }
1039
1112
  }
1040
- catch {
1041
- // Silent — codeStore failure falls back to inline-base64 path.
1113
+ catch (err) {
1114
+ // Named for the OUTCOME, not the call: a throwing
1115
+ // `renderStore.get` or `hashOf` reaches here too, and the
1116
+ // consequence is identical — this envelope has no codeUrl.
1117
+ // `error` carries which one it was.
1118
+ reportRenderCodeWriteFailed({
1119
+ sessionId,
1120
+ appId: ctx.appId,
1121
+ liveChannelWired: deps.mintWsToken !== undefined,
1122
+ cause: err,
1123
+ });
1042
1124
  }
1043
1125
  }
1044
1126
  // Per-render theme overlay. The commit paths above
@@ -1064,11 +1146,16 @@ export function createGguiRenderHandler(deps) {
1064
1146
  top.type !== 'mcpApps' &&
1065
1147
  top.type !== 'system') {
1066
1148
  const overlaid = { ...top, themeId: parsed.themeId };
1067
- await deps.renderStore.commit({
1149
+ const committed = await deps.renderStore.commit({
1068
1150
  render: overlaid,
1069
1151
  appId: ctx.appId,
1070
1152
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1071
1153
  });
1154
+ // Last commit of the render when a theme override is in
1155
+ // play, and every reuse / cold-gen path has settled by
1156
+ // here — so this write carries the final blueprint id
1157
+ // (already resolved before the success commit, #460).
1158
+ await identityWriterFor(resolvedBlueprintId ?? null)(committed);
1072
1159
  }
1073
1160
  }
1074
1161
  catch (err) {
@@ -1076,14 +1163,6 @@ export function createGguiRenderHandler(deps) {
1076
1163
  console.warn('[ggui_render.theme_overlay_failed]', err instanceof Error ? err.message : String(err));
1077
1164
  }
1078
1165
  }
1079
- // Canonical-key of the resolved contract. Surface on output so
1080
- // downstream consumers (resultMeta builder, cache trace,
1081
- // resource URI minter) read from a single computed value
1082
- // instead of recomputing against the same canonicalization.
1083
- // This is the same hash the handshake returned as
1084
- // `contractHash`. The variant axis pairs with this on the wire
1085
- // output via `effectiveVariantKey` (computed once up top).
1086
- const resolvedContractHash = blueprintKey(effectiveContract);
1087
1166
  // ── Failure envelope (SPEC §7.1) ──────────────────────────────
1088
1167
  //
1089
1168
  // A failed/rejected generation returns the in-result failure
@@ -1106,7 +1185,7 @@ export function createGguiRenderHandler(deps) {
1106
1185
  shortCode,
1107
1186
  codeReady: false,
1108
1187
  handshakeId: handshakeRecord.handshakeId,
1109
- contractHash: resolvedContractHash,
1188
+ contractHash: effectiveContractKey,
1110
1189
  // Present-on-materialisation (§9.1): no component
1111
1190
  // materialised, so the id is the empty sentinel.
1112
1191
  blueprintId: '',
@@ -1128,7 +1207,7 @@ export function createGguiRenderHandler(deps) {
1128
1207
  ctx,
1129
1208
  sessionId,
1130
1209
  contract: effectiveContract,
1131
- contractHash: resolvedContractHash,
1210
+ contractHash: effectiveContractKey,
1132
1211
  intent: story.intent,
1133
1212
  action,
1134
1213
  codeReady: false,
@@ -1163,8 +1242,8 @@ export function createGguiRenderHandler(deps) {
1163
1242
  // structuredContent too lets agent SDKs that strip `_meta` from
1164
1243
  // tool_results (OpenAI Agents SDK, Google ADK) still hand a
1165
1244
  // mount handle to their frontend without the side-channel.
1166
- const blueprintSegmentForOutput = resolvedContractHash
1167
- ? `/${resolvedContractHash}`
1245
+ const blueprintSegmentForOutput = effectiveContractKey
1246
+ ? `/${effectiveContractKey}`
1168
1247
  : '';
1169
1248
  const resourceUriForOutput = `${GGUI_RENDER_UI_META.resourceUri}/${sessionId}${blueprintSegmentForOutput}`;
1170
1249
  const result = {
@@ -1174,7 +1253,7 @@ export function createGguiRenderHandler(deps) {
1174
1253
  shortCode,
1175
1254
  codeReady: generatedCodeReady,
1176
1255
  handshakeId: handshakeRecord.handshakeId,
1177
- contractHash: resolvedContractHash,
1256
+ contractHash: effectiveContractKey,
1178
1257
  // Reuse → stored UUID (§6 point-read); cold-gen → minted UUID
1179
1258
  // (`safelyRegisterBlueprint`). Empty only on the
1180
1259
  // genuinely-no-component branches (probe-card / generation-off),
@@ -1200,7 +1279,7 @@ export function createGguiRenderHandler(deps) {
1200
1279
  ctx,
1201
1280
  sessionId,
1202
1281
  contract: effectiveContract,
1203
- contractHash: resolvedContractHash,
1282
+ contractHash: effectiveContractKey,
1204
1283
  intent: story.intent,
1205
1284
  action,
1206
1285
  codeReady: generatedCodeReady,
@@ -1416,7 +1495,13 @@ export function createGguiRenderHandler(deps) {
1416
1495
  * semantics; anything shorter would close active renders mid-lifecycle.
1417
1496
  */
1418
1497
  const DEFAULT_RENDER_TTL_MS = 60 * 60 * 1000;
1419
- async function runGenerationIntoGguiSession(generation, renderStore, previewDeps, channelNotifier, checkRenderContracts, generatorOverride, args) {
1498
+ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps, channelNotifier, checkRenderContracts, generatorOverride,
1499
+ /**
1500
+ * Render-row retention window in ms — forwarded from
1501
+ * `GguiRenderHandlerDeps.renderTtlMs`. `undefined` falls back to
1502
+ * `DEFAULT_RENDER_TTL_MS` at each commit site below.
1503
+ */
1504
+ renderTtlMs, args) {
1420
1505
  const { ctx, sessionId, story } = args;
1421
1506
  const nowIso = new Date().toISOString();
1422
1507
  const nowEpochMs = Date.now();
@@ -1449,13 +1534,14 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1449
1534
  result = await generatorOverride(generateInputBase, ctx);
1450
1535
  }
1451
1536
  catch (err) {
1452
- return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, {
1537
+ return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, renderTtlMs, {
1453
1538
  sessionId,
1454
1539
  appId: ctx.appId,
1455
1540
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1456
1541
  story,
1457
1542
  nowIso,
1458
1543
  nowEpochMs,
1544
+ writeIdentity: args.writeIdentityFor(null),
1459
1545
  message: err instanceof Error
1460
1546
  ? `generator threw: ${err.message}`
1461
1547
  : 'generator threw',
@@ -1472,13 +1558,14 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1472
1558
  creds = await generation.resolveLlm(ctx);
1473
1559
  }
1474
1560
  catch (err) {
1475
- return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, {
1561
+ return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, renderTtlMs, {
1476
1562
  sessionId,
1477
1563
  appId: ctx.appId,
1478
1564
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1479
1565
  story,
1480
1566
  nowIso,
1481
1567
  nowEpochMs,
1568
+ writeIdentity: args.writeIdentityFor(null),
1482
1569
  message: err instanceof Error
1483
1570
  ? `credential resolution failed: ${err.message}`
1484
1571
  : 'credential resolution failed',
@@ -1508,16 +1595,18 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1508
1595
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1509
1596
  nowIso,
1510
1597
  render: fallback,
1598
+ writeIdentity: args.writeIdentityFor(null),
1511
1599
  });
1512
1600
  }
1513
1601
  }
1514
- return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, {
1602
+ return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, renderTtlMs, {
1515
1603
  sessionId,
1516
1604
  appId: ctx.appId,
1517
1605
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1518
1606
  story,
1519
1607
  nowIso,
1520
1608
  nowEpochMs,
1609
+ writeIdentity: args.writeIdentityFor(null),
1521
1610
  message: 'no credentials available for the configured generation provider (expected env var or ~/.ggui/credentials.json entry)',
1522
1611
  code: 'NO_CREDENTIALS',
1523
1612
  reason: 'no-credentials',
@@ -1532,13 +1621,14 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1532
1621
  });
1533
1622
  }
1534
1623
  catch (err) {
1535
- return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, {
1624
+ return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, renderTtlMs, {
1536
1625
  sessionId,
1537
1626
  appId: ctx.appId,
1538
1627
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1539
1628
  story,
1540
1629
  nowIso,
1541
1630
  nowEpochMs,
1631
+ writeIdentity: args.writeIdentityFor(null),
1542
1632
  message: err instanceof Error
1543
1633
  ? `generator threw: ${err.message}`
1544
1634
  : 'generator threw',
@@ -1549,13 +1639,14 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1549
1639
  }
1550
1640
  }
1551
1641
  if (!result.ok) {
1552
- return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, {
1642
+ return commitErrorGguiSession(renderStore, previewDeps, channelNotifier, renderTtlMs, {
1553
1643
  sessionId,
1554
1644
  appId: ctx.appId,
1555
1645
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1556
1646
  story,
1557
1647
  nowIso,
1558
1648
  nowEpochMs,
1649
+ writeIdentity: args.writeIdentityFor(null),
1559
1650
  message: result.error.message,
1560
1651
  // Canonical projection — cloud seam codes (VALIDATION_ERROR /
1561
1652
  // NO_PLATFORM_KEY / PRODUCTION_FAILED) map through 1:1;
@@ -1576,7 +1667,7 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1576
1667
  contentType: 'application/javascript+react',
1577
1668
  createdAt: nowEpochMs,
1578
1669
  lastActivityAt: nowEpochMs,
1579
- expiresAt: nowEpochMs + DEFAULT_RENDER_TTL_MS,
1670
+ expiresAt: nowEpochMs + (renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
1580
1671
  eventSequence: 0,
1581
1672
  ...(result.response.warnings && result.response.warnings.length > 0
1582
1673
  ? { description: result.response.warnings[0] }
@@ -1617,12 +1708,30 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1617
1708
  throw err;
1618
1709
  }
1619
1710
  }
1711
+ // #460 — resolve the blueprint id BEFORE the commit. Registration
1712
+ // was already awaited on this path (it just ran after the commit),
1713
+ // so the reorder adds no latency and deletes the get+put backfill —
1714
+ // and with it the backfill's stale-write race against the pod's
1715
+ // field-targeted seq advance.
1716
+ const producedSource = {
1717
+ kind: 'llm',
1718
+ generator: result.metadata.generator,
1719
+ model: result.metadata.model,
1720
+ };
1721
+ let resolvedBlueprintId;
1722
+ if (args.resolveBlueprintId) {
1723
+ resolvedBlueprintId = await args.resolveBlueprintId({
1724
+ componentCode: result.response.componentCode,
1725
+ source: producedSource,
1726
+ });
1727
+ }
1620
1728
  try {
1621
- await renderStore.commit({
1729
+ const committed = await renderStore.commit({
1622
1730
  render: componentRender,
1623
1731
  appId: ctx.appId,
1624
1732
  userId: ctx.userId, // per-user isolation (undefined for non-federated single-user)
1625
1733
  });
1734
+ await args.writeIdentityFor(resolvedBlueprintId ?? null)(committed);
1626
1735
  }
1627
1736
  catch {
1628
1737
  await safelyFinalizePreview(previewDeps, sessionId, 'commit-failed');
@@ -1645,11 +1754,10 @@ async function runGenerationIntoGguiSession(generation, renderStore, previewDeps
1645
1754
  ok: true,
1646
1755
  componentCode: result.response.componentCode,
1647
1756
  createdAt: nowIso,
1648
- source: {
1649
- kind: 'llm',
1650
- generator: result.metadata.generator,
1651
- model: result.metadata.model,
1652
- },
1757
+ source: producedSource,
1758
+ ...(resolvedBlueprintId !== undefined
1759
+ ? { blueprintId: resolvedBlueprintId }
1760
+ : {}),
1653
1761
  };
1654
1762
  }
1655
1763
  /**
@@ -1670,12 +1778,13 @@ async function commitNoCredentialsCardGguiSession(renderStore, previewDeps, chan
1670
1778
  const render = { ...args.render, id: args.sessionId };
1671
1779
  let committed = false;
1672
1780
  try {
1673
- await renderStore.commit({
1781
+ const stored = await renderStore.commit({
1674
1782
  render,
1675
1783
  appId: args.appId,
1676
1784
  userId: args.userId,
1677
1785
  });
1678
1786
  committed = true;
1787
+ await args.writeIdentity(stored);
1679
1788
  }
1680
1789
  catch {
1681
1790
  // Commit rejected — preview teardown is the only honest recovery;
@@ -1709,7 +1818,9 @@ async function commitNoCredentialsCardGguiSession(renderStore, previewDeps, chan
1709
1818
  * `componentCode === ''` by showing a fallback UI; the extra `error`
1710
1819
  * field carries the operator-facing reason.
1711
1820
  */
1712
- async function commitErrorGguiSession(renderStore, previewDeps, channelNotifier, args) {
1821
+ async function commitErrorGguiSession(renderStore, previewDeps, channelNotifier,
1822
+ /** Forwarded from `GguiRenderHandlerDeps.renderTtlMs` (see there). */
1823
+ renderTtlMs, args) {
1713
1824
  const errorRender = {
1714
1825
  id: args.sessionId,
1715
1826
  appId: args.appId,
@@ -1720,18 +1831,19 @@ async function commitErrorGguiSession(renderStore, previewDeps, channelNotifier,
1720
1831
  contentType: 'application/javascript+react',
1721
1832
  createdAt: args.nowEpochMs,
1722
1833
  lastActivityAt: args.nowEpochMs,
1723
- expiresAt: args.nowEpochMs + DEFAULT_RENDER_TTL_MS,
1834
+ expiresAt: args.nowEpochMs + (renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
1724
1835
  eventSequence: 0,
1725
1836
  ...(args.appTheme !== undefined ? { theme: args.appTheme } : {}),
1726
1837
  };
1727
1838
  let committed = false;
1728
1839
  try {
1729
- await renderStore.commit({
1840
+ const stored = await renderStore.commit({
1730
1841
  render: errorRender,
1731
1842
  appId: args.appId,
1732
1843
  userId: args.userId,
1733
1844
  });
1734
1845
  committed = true;
1846
+ await args.writeIdentity(stored);
1735
1847
  }
1736
1848
  catch {
1737
1849
  // Secondary failure — render store rejected the error record.
@@ -1810,7 +1922,9 @@ function isJsonObject(v) {
1810
1922
  * returns `codeReady: false` so the agent observes the degraded
1811
1923
  * state through the channel instead of a synthetic "ready" signal).
1812
1924
  */
1813
- async function commitCachedGguiSession(renderStore, previewDeps, channelNotifier, checkRenderContracts, args) {
1925
+ async function commitCachedGguiSession(renderStore, previewDeps, channelNotifier, checkRenderContracts,
1926
+ /** Forwarded from `GguiRenderHandlerDeps.renderTtlMs` (see there). */
1927
+ renderTtlMs, args) {
1814
1928
  const nowEpochMs = Date.now();
1815
1929
  // Cached path — project optional contract fields onto the
1816
1930
  // ComponentGguiSession so the bootstrap-meta derivation in `resultMeta`
@@ -1824,7 +1938,7 @@ async function commitCachedGguiSession(renderStore, previewDeps, channelNotifier
1824
1938
  contentType: 'application/javascript+react',
1825
1939
  createdAt: nowEpochMs,
1826
1940
  lastActivityAt: nowEpochMs,
1827
- expiresAt: nowEpochMs + DEFAULT_RENDER_TTL_MS,
1941
+ expiresAt: nowEpochMs + (renderTtlMs ?? DEFAULT_RENDER_TTL_MS),
1828
1942
  eventSequence: 0,
1829
1943
  ...(args.runtimeProps !== undefined
1830
1944
  ? { props: args.runtimeProps }
@@ -1862,11 +1976,12 @@ async function commitCachedGguiSession(renderStore, previewDeps, channelNotifier
1862
1976
  }
1863
1977
  }
1864
1978
  try {
1865
- await renderStore.commit({
1979
+ const committed = await renderStore.commit({
1866
1980
  render: componentRender,
1867
1981
  appId: args.appId,
1868
1982
  userId: args.userId,
1869
1983
  });
1984
+ await args.writeIdentity(committed);
1870
1985
  }
1871
1986
  catch {
1872
1987
  await safelyFinalizePreview(previewDeps, args.sessionId, 'commit-failed');
@@ -52,7 +52,7 @@
52
52
  * IS the addressable scope.
53
53
  */
54
54
  import { z } from 'zod';
55
- import type { GguiSessionStore } from '@ggui-ai/mcp-server-core';
55
+ import type { GguiSessionStore, RenderIdentityStore } from '@ggui-ai/mcp-server-core';
56
56
  import type { SharedHandler } from '../types.js';
57
57
  declare const inputSchema: {
58
58
  readonly sessionId: z.ZodString;
@@ -80,6 +80,18 @@ interface SyncContextRejected {
80
80
  type SyncContextOutput = SyncContextAccepted | SyncContextRejected;
81
81
  export interface CreateGguiSyncContextHandlerDeps {
82
82
  readonly renderStore: GguiSessionStore;
83
+ /**
84
+ * Durable render-identity side store. A snapshot sync re-commits the
85
+ * render row, so when a record exists its freshness stamp is
86
+ * refreshed off the committed row.
87
+ *
88
+ * Note what this does NOT persist: the record carries no
89
+ * `contextSnapshot`, so the snapshot itself stays on the render row
90
+ * alone. Identity fields are never recomputed here, and a render
91
+ * with no record yet is skipped rather than synthesized. Writes are
92
+ * best-effort and can never fail the sync.
93
+ */
94
+ readonly renderIdentityStore?: RenderIdentityStore;
83
95
  }
84
96
  /**
85
97
  * Build the `ggui_runtime_sync_context` handler. Registers as app-visible so
@@ -1 +1 @@
1
- {"version":3,"file":"sync-context.d.ts","sourceRoot":"","sources":["../../src/renders/sync-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,QAAA,MAAM,WAAW;;;;CAyBP,CAAC;AAEX,QAAA,MAAM,YAAY;;;;;;;;;CAWR,CAAC;AAEX,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB;AACD,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EACT,mBAAmB,GACnB,iBAAiB,GACjB,0BAA0B,GAC1B,mBAAmB,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AACD,KAAK,iBAAiB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAEnE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,gCAAgC,GACrC,aAAa,CAAC,OAAO,WAAW,EAAE,OAAO,YAAY,EAAE,iBAAiB,CAAC,CAmH3E"}
1
+ {"version":3,"file":"sync-context.d.ts","sourceRoot":"","sources":["../../src/renders/sync-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,QAAA,MAAM,WAAW;;;;CAyBP,CAAC;AAEX,QAAA,MAAM,YAAY;;;;;;;;;CAWR,CAAC;AAEX,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB;AACD,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EACT,mBAAmB,GACnB,iBAAiB,GACjB,0BAA0B,GAC1B,mBAAmB,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AACD,KAAK,iBAAiB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAEnE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CACpD;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,gCAAgC,GACrC,aAAa,CAAC,OAAO,WAAW,EAAE,OAAO,YAAY,EAAE,iBAAiB,CAAC,CAwH3E"}