From e31c63816deb916d777a506dee3a0a7b808745a0 Mon Sep 17 00:00:00 2001 From: "Syring, Nikolas" Date: Sat, 25 Apr 2026 18:00:38 +0200 Subject: [PATCH] fix(spaces): keep Current Widget transient fresh and trim-protected The Current Widget transient envelope is the authoritative source the agent uses to build exact-snippet patches. Three bugs let trimming and stale state corrupt that source: 1. readWidget did not refresh the transient. Its result landed in chat history only, where the long-message middle-replacement could later inject the placeholder string into the visible widget source. The agent then either copied that placeholder into a `find` snippet (patch fails because the text does not exist in storage) or pasted it into a renderWidget(...) body (the placeholder reads as a JavaScript syntax error - "Unexpected identifier 'characters'" - and the renderer crashes on first execution). 2. A failed patchWidget / renderWidget / upsertWidget left the transient stale. The runtime validator throws (overlapping edits, "patchWidget is for partial only", missing find snippet) before buildWidgetToolResult runs, so the transient still reflects the prior turn or is missing entirely. The agent retries against an out-of-date or absent source, often falling back to a full renderWidget rewrite. 3. The transient itself was trim-eligible by default. Even with read and failed-patch refreshes wired up, the prompt-budget trimmer would still mid-replace the source content under pressure, reintroducing the placeholder-in-renderer corruption. Three changes close the loop: - New refreshCurrentWidgetTransientFromStorage(...) helper reads the widget from storage and republishes the Current Widget section. Called from readWidget on success, and from patchWidget, renderWidget, and upsertWidget try/catch blocks before re-throwing. Refresh failures are silent so the original error is never masked. - Current Widget transient section is now marked trimAllowed: false. Comment at the set-site documents why (placeholder-as-renderer syntax error). The onscreen-agent transient runtime and prompt-item normalizer thread the flag through to the agent_prompt applyPromptPartBudget step, which already honored trimAllowed: false but had no caller passing it through. The change is scoped to onscreen agent surface only. Admin chat does not currently engage the long-message trimmer (it concatenates transient sections directly in api.js:formatTransientMessageBlock). If admin chat later adopts a trimmer, that PR would need to wire trimAllowed through its own normalizeTransientSection in the same pattern; this is intentionally left to that future change rather than adding dead code here. Verified end-to-end with both gpt-5.4 (Codex provider) and a local qwen3-coder model: repeated targeted patchWidget edits across multiple turns without falling back to renderWidget rewrites; transient source remains byte-stable across budget pressure. --- app/L0/_all/mod/_core/onscreen_agent/llm.js | 18 ++- app/L0/_all/mod/_core/onscreen_agent/store.js | 8 +- app/L0/_all/mod/_core/spaces/store.js | 123 +++++++++++++++--- 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/app/L0/_all/mod/_core/onscreen_agent/llm.js b/app/L0/_all/mod/_core/onscreen_agent/llm.js index c761436e..6d1b826b 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/llm.js +++ b/app/L0/_all/mod/_core/onscreen_agent/llm.js @@ -422,12 +422,18 @@ function normalizeTransientSection(section, fallbackKey = "") { return null; } - return { + const normalized = { content, heading: heading || key, key, order }; + + if (section?.trimAllowed === false) { + normalized.trimAllowed = false; + } + + return normalized; } function normalizeTransientSections(sections) { @@ -456,13 +462,19 @@ function createTransientPromptItem(section = {}, fallbackKey = "") { return null; } - return normalizePromptItemDefinition(normalizedSection.key, { + const definition = { heading: normalizedSection.heading, key: normalizedSection.key, order: normalizedSection.order, trimPriority: Number.isFinite(section?.trimPriority) ? Number(section.trimPriority) : 0, value: normalizedSection.content - }); + }; + + if (normalizedSection.trimAllowed === false || section?.trimAllowed === false) { + definition.trimAllowed = false; + } + + return normalizePromptItemDefinition(normalizedSection.key, definition); } function normalizeTransientItems(items = {}) { diff --git a/app/L0/_all/mod/_core/onscreen_agent/store.js b/app/L0/_all/mod/_core/onscreen_agent/store.js index 4d592c99..bfe0abea 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/store.js +++ b/app/L0/_all/mod/_core/onscreen_agent/store.js @@ -319,12 +319,18 @@ function normalizeTransientSection(section, fallbackKey = "") { return null; } - return { + const normalized = { content, heading: heading || key, key, order }; + + if (section?.trimAllowed === false) { + normalized.trimAllowed = false; + } + + return normalized; } function cloneTransientSection(section) { diff --git a/app/L0/_all/mod/_core/spaces/store.js b/app/L0/_all/mod/_core/spaces/store.js index 84d61cfe..941e5226 100644 --- a/app/L0/_all/mod/_core/spaces/store.js +++ b/app/L0/_all/mod/_core/spaces/store.js @@ -574,12 +574,22 @@ function ensureSpacesRuntimeNamespace() { throw new Error("A target spaceId is required to render a widget."); } - const result = await upsertWidget({ - ...(await applyAutoWidgetPlacementToRequest(request, targetSpaceId)), - name: request.name ?? request.title, - spaceId: targetSpaceId, - widgetId: request.widgetId ?? request.id - }); + let result; + + try { + result = await upsertWidget({ + ...(await applyAutoWidgetPlacementToRequest(request, targetSpaceId)), + name: request.name ?? request.title, + spaceId: targetSpaceId, + widgetId: request.widgetId ?? request.id + }); + } catch (error) { + await refreshCurrentWidgetTransientFromStorage({ + spaceId: targetSpaceId, + widgetId: request.widgetId ?? request.id + }); + throw error; + } if (activeSpacesStore) { await activeSpacesStore.handleExternalMutation(targetSpaceId, { @@ -769,10 +779,20 @@ function ensureSpacesRuntimeNamespace() { throw new Error("A target spaceId is required to patch a widget."); } - const result = await patchWidgetFromStorage({ - ...options, - spaceId: targetSpaceId - }); + let result; + + try { + result = await patchWidgetFromStorage({ + ...options, + spaceId: targetSpaceId + }); + } catch (error) { + await refreshCurrentWidgetTransientFromStorage({ + spaceId: targetSpaceId, + widgetId: options.widgetId + }); + throw error; + } if (activeSpacesStore) { await activeSpacesStore.handleExternalMutation(targetSpaceId, { @@ -838,10 +858,20 @@ function ensureSpacesRuntimeNamespace() { throw new Error("A target spaceId is required to save a widget."); } - const result = await upsertWidget({ - ...(await applyAutoWidgetPlacementToRequest(options, targetSpaceId)), - spaceId: targetSpaceId - }); + let result; + + try { + result = await upsertWidget({ + ...(await applyAutoWidgetPlacementToRequest(options, targetSpaceId)), + spaceId: targetSpaceId + }); + } catch (error) { + await refreshCurrentWidgetTransientFromStorage({ + spaceId: targetSpaceId, + widgetId: options.widgetId ?? options.id + }); + throw error; + } if (activeSpacesStore && options.refresh !== false) { await activeSpacesStore.handleExternalMutation(targetSpaceId, { @@ -1123,11 +1153,22 @@ function updateCurrentWidgetTransientSection({ return false; } + // Mark this section as not trim-eligible. The Current Widget envelope is + // the authoritative source the agent uses to build exact-snippet patches; + // mid-content replacement by `trimPromptLongMessage(...)` would inject the + // placeholder string into the visible widget source. The agent then either + // copies that placeholder into a `find` snippet (patch fails because the + // text does not exist in storage) or pastes it into a `renderWidget(...)` + // body (the placeholder reads as a JavaScript syntax error and the + // renderer crashes on first execution with `Unexpected identifier + // 'characters'`). Trimming this source therefore breaks the only contract + // that lets the agent patch a widget at all. transient.set(CURRENT_WIDGET_TRANSIENT_KEY, { content, heading: "Current Widget", key: CURRENT_WIDGET_TRANSIENT_KEY, - order: 300 + order: 300, + trimAllowed: false }); return true; } @@ -1183,6 +1224,50 @@ function emitWidgetReadToolResult(widgetText = "", widgetId = "") { return typeof widgetText === "string" ? widgetText : ""; } +async function refreshCurrentWidgetTransientFromStorage({ spaceId = "", widgetId = "" } = {}) { + const normalizedSpaceId = normalizeOptionalSpaceId(spaceId); + const normalizedWidgetId = normalizeOptionalWidgetId(widgetId); + + if (!normalizedSpaceId || !normalizedWidgetId) { + return false; + } + + let widgetText = ""; + + try { + widgetText = await readWidgetFromStorage({ + spaceId: normalizedSpaceId, + widgetName: normalizedWidgetId + }); + } catch { + return false; + } + + if (typeof widgetText !== "string" || !widgetText.trim()) { + return false; + } + + const widgetRender = getWidgetRenderCheckForSpace(normalizedSpaceId, normalizedWidgetId); + const widgetView = readMountedWidgetHtmlEnvelope({ + full: false, + spaceId: normalizedSpaceId, + widgetId: normalizedWidgetId, + widgetRender + }); + const widgetPath = buildSpaceWidgetFilePath(normalizedSpaceId, normalizedWidgetId); + + return updateCurrentWidgetTransientSection({ + spaceId: normalizedSpaceId, + widgetId: normalizedWidgetId, + widgetPath, + widgetStatusText: "", + widgetHtml: widgetView.html, + widgetHtmlAvailable: widgetView.available, + widgetHtmlUnavailableReason: widgetView.unavailableReason, + widgetText + }); +} + function emitWidgetSeeToolResult(widgetHtml = "", widgetId = "", full = false) { const normalizedWidgetId = normalizeOptionalWidgetId(widgetId); const statusText = normalizedWidgetId @@ -1665,6 +1750,14 @@ function createCurrentSpaceRuntime(namespace) { widgetName }); const widgetId = extractWidgetIdFromWidgetText(widgetText) || normalizeOptionalWidgetId(widgetName); + + if (spaceId && widgetId) { + await refreshCurrentWidgetTransientFromStorage({ + spaceId, + widgetId + }); + } + return emitWidgetReadToolResult(widgetText, widgetId); })(); },