feat(widgets): consecutive-patch loop guard in widget operation status#34
Open
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Open
feat(widgets): consecutive-patch loop guard in widget operation status#34nsyring wants to merge 1 commit intoagent0ai:mainfrom
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Conversation
Surfaces a soft warning in the patchWidget status text after the third consecutive patch on the same widget id without an intervening readWidget, renderWidget, upsertWidget, reloadWidget, or removeWidget on that id. The patch still applies; the result envelope is unchanged; no skill rules or APIs are added. The warning is appended after the existing terminator (loaded to TRANSIENT. / done.) so consumers grepping the existing status text still match. Why: a recurring pathology in agent traces is the iterative-tweak loop where the agent treats technical patch success as outcome success and keeps patching after each "rendered ok" without verifying visually. After 5-10 patches the renderer accumulates unrelated drift, the agent spends tokens, and the original problem persists. The widget skill already requires a fresh source view before mutating, but until now there was no runtime push-back when the agent ignored that rule. Design choices: - Soft warning, not hard veto. A veto would risk false positives when the user explicitly asks for an iterative tweak loop; a note in the status is enough to nudge mode-switch behavior in the agent. - Threshold = 3, matching the existing widget skill convention from PR agent0ai#6 ("if the same error repeats twice, stop and call readWidget"). Centralized as CONSECUTIVE_PATCH_WARNING_THRESHOLD for a one-line future tuning pass. - Per (spaceId, widgetId), not global. Patches on a different widget do not reset another widget's streak so multi-widget chats keep their own counters. - Page-lifetime state (a module-level Map). A page reload starts fresh, matching the existing transient runtime and prompt-builder caches. Single-file change in spaces/store.js. New helpers recordWidgetOperationForLoopGuard, clearWidgetOperationLoopGuard, formatOrdinalSuffix, formatConsecutivePatchWarning. The format helper emits "1st"/"2nd"/"3rd"/"11th"/"21st" correctly. buildWidgetToolResult records the operation and forwards the count to formatWidgetOperationStatusText; readWidget and the three remove-paths clear the counter alongside the existing transient-section clear.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(widgets): consecutive-patch loop guard in widget operation status
Summary
Adds a soft-warning fragment to the widget operation status text after the third consecutive
patchWidgetcall on the same widget id without an interveningreadWidget,renderWidget,upsertWidget,reloadWidget, orremoveWidgeton that id. The guard never refuses or throws — the patch still applies — but the agent and a reviewer reading the chat see at the bottom of the status:The threshold count, the rules for what resets the streak, and the soft-warning wording are all kept in one small block of pure helpers in
app/L0/_all/mod/_core/spaces/store.js. No skill rule changes, no API additions, no caller updates required.Why
A recurring failure mode I reproduced repeatedly during local widget development: the agent finds something visually off, patches it, the patch reports
rendered ok, but the user-visible problem is unchanged (or barely changed). The agent treats technical patch success as confirmation of outcome success, patches again with a slightly different tweak, repeats. After 5–10 patches the renderer has accumulated unrelated drift, the agent has spent a lot of tokens, and the original problem is still there.Quoting the agent's own self-reflection on the loop:
The widget skill already says (in
space-widgets/SKILL.mdand the read-before-mutate rules insystem-prompt.md) that the agent should hold a fresh source view before mutating. But that contract is on the prompt-facing side. There was no runtime signal pushing back when the agent ignored the rule. This PR adds that signal, conservatively.Design choices
Soft warning, not hard veto. The fragment is appended to the status text as
Note: …. The patch still applies; the result envelope is unchanged; existing skill rules (fresh read then do it,patch error loop prevention,framework-corrected rewrite continues, etc.) keep their semantics. A hard veto would risk false positives when the user explicitly asks for an iterative tweak loop.Threshold = 3. Matches the existing convention in PR #6 ("if the same error repeats twice on the same widget, stop and call readWidget"), which already trains the agent to expect 3 as a friction point. Centralized as the
CONSECUTIVE_PATCH_WARNING_THRESHOLDconstant so a future tuning pass is one line.What resets the streak:
readWidget(id)success on the same id — the agent acknowledged the on-disk stateseeWidget(id)success on the same id — the agent performed the visual verification the warning explicitly asks forrenderWidget(id),upsertWidget(id),reloadWidget(id),removeWidget(id)— any non-patch operation on that idremoveWidgets,removeAllWidgets— clears all affected idsWhat does not reset:
(spaceId, widgetId), not global.State scope. The counter map is a module-level
Mapinspaces/store.js. The state lives for the page lifetime — same scope as the existing transient runtime, the empty-canvas-seen storage, and the prompt-builder caches. A page reload starts fresh, which is the right behavior.What changed
app/L0/_all/mod/_core/spaces/store.js:CONSECUTIVE_PATCH_WARNING_THRESHOLD = 3andconsecutivePatchCountByWidgetKey = new Map().buildConsecutivePatchKey(spaceId, widgetId)returns the map key (or empty if either id is missing).recordWidgetOperationForLoopGuard(spaceId, widgetId, operationLabel)increments the counter forpatchWidget(...)operations and resets it for any other operation on that id. Returns the new count for the caller.clearWidgetOperationLoopGuard(spaceId, widgetId)clears the counter explicitly, called fromreadWidgetand from the remove-widget paths.formatOrdinalSuffix(value)produces1st/2nd/3rd/11th/21st/etc. for the warning text.formatConsecutivePatchWarning(consecutivePatchCount)returns the fragment text when count ≥ threshold; otherwise empty.formatWidgetOperationStatusText(...)accepts a newconsecutivePatchCountoption and appends" Note: <warning>."at the end of the status sentence — after the period that ends the existing sentence — so consumers grepping the existing terminator-style suffix (loaded to TRANSIENT.,done.) still match.buildWidgetToolResult(...)callsrecordWidgetOperationForLoopGuard(...)before formatting status and forwards the count to the formatter.readWidget(...)callsclearWidgetOperationLoopGuard(...)after a successful storage read.removeWidget/removeWidgets/removeAllWidgetspaths now also callclearWidgetOperationLoopGuard(...)alongside the existingclearCurrentWidgetTransientSection(...).Single-file change. Net +86 / −5.
Behavior matrix
patchpatch,patchpatch,patch,patchNote: 3rd consecutive patch on this widget without a fresh readWidget — verify visually or report current state instead of patching again.patch,patch,patch,patchNote: 4th consecutive patch on this widget …patch,patch,read,patchpatch,patch,render,patchpatch X,patch X,patch Y,patch XXpatch showsNote: 3rd…;Ypatch shows nothingpatch X(different space)Test plan
node --checkpassesnpm run desktop:packbuilds:readWidgetand the streak resetsOut of scope (possible follow-ups)
Note: Nth consecutive patch …, the next move must bereadWidget(id)or a textual state report"). Adding such a rule needs a separate prompt-engineering pass and a careful read of the existing turn-rule list to avoid contradictions.space.spaces.consecutivePatchWarningThresholdif a deployment needs different sensitivity.consecutivePatchCountfield on the result envelope (alongside the status fragment). Skill rules that want to react programmatically would benefit, but the current status-fragment surface is sufficient for the pathology this PR addresses.🤖 Generated with Claude Code