{"record":{"id":"02c2859a35e565d7","repo":"can1357/oh-my-pi","slug":"operation-operationnumber-has-an-unclosed-selec","errorCode":null,"errorMessage":"Operation ${operationNumber} has an unclosed selection marker ⟪; add closing ⟫. | Operation ${operationNumber} has an unmatched closing selection marker ⟫; add opening ⟪.","messagePattern":"Operation (.+?) has an unclosed selection marker ⟪; add closing ⟫\\. \\| Operation (.+?) has an unmatched closing selection marker ⟫; add opening ⟪\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/edit/sloppy.ts","lineNumber":580,"sourceCode":"\t\t\tselected = true;\n\t\t\tindex += SELECT_OPEN.length;\n\t\t} else if (pattern.startsWith(SELECT_CLOSE, index)) {\n\t\t\tselected = false;\n\t\t\tindex += SELECT_CLOSE.length;\n\t\t} else if (selected && pattern.startsWith(SELECT_DIVIDER, index)) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\tindex++;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction validateSelectionMarkers(pattern: string, operationNumber: number): void {\n\tconst openCount = (pattern.match(/⟪/gu) || []).length;\n\tconst closeCount = (pattern.match(/⟫/gu) || []).length;\n\tif (openCount === closeCount) return;\n\tthrow new Error(\n\t\topenCount > closeCount\n\t\t\t? `Operation ${operationNumber} has an unclosed selection marker ⟪; add closing ⟫.`\n\t\t\t: `Operation ${operationNumber} has an unmatched closing selection marker ⟫; add opening ⟪.`,\n\t);\n}\n\n/**\n * Resolve a selection whose content contains the divider character itself\n * (box-drawing code). A trailing divider reads as a deletion of the content\n * before it with the inner dividers literal; an odd count splits at the\n * middle divider (a symmetric replacement keeps equal literals on each\n * side); an even count reads as a deletion of the whole selected text. A\n * wrong guess still fails loud downstream: the resolved old side must match\n * the file.\n */\nfunction resolveLiteralDividers(\n\tselected: string,\n\toperationNumber: number,","sourceCodeStart":562,"sourceCodeEnd":598,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/edit/sloppy.ts#L562-L598","documentation":"validateSelectionMarkers counts ⟪ open and ⟫ close markers in an operation pattern and requires them to balance. Selection markers denote 'replace exactly this span'; an unbalanced count means the model either never closed a selection or closed one it never opened, so the replacement target is ambiguous.","triggerScenarios":"Calling applySloppy with an operation whose pattern contains an odd/extra ⟪ without ⟫ (unclosed) or an ⟫ without ⟪ (unmatched closing).","commonSituations":"An LLM truncates output mid-selection; a stray ⟫ character in code being quoted; copy/paste dropping the closing marker line.","solutions":["Add the missing ⟫ after the selected text for every unclosed ⟪","Delete the stray ⟫ or add the missing ⟪ before it","Re-send the full corrected payload — ops apply atomically, so all operations must be re-submitted"],"exampleFix":"// before\n«old ⟪text»new\n// after\n«old ⟪text⟫»new","handlingStrategy":"validation","validationCode":"const opens = (pattern.match(/⟪/gu) ?? []).length;\nconst closes = (pattern.match(/⟫/gu) ?? []).length;\nif (opens !== closes) throw new Error(`unbalanced selection markers: ${opens} ⟪ vs ${closes} ⟫`);","typeGuard":null,"tryCatchPattern":"try {\n  applySloppy(content, input, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('selection marker')) {\n    // fix markers in payload and re-send full corrected payload\n  } else throw err;\n}","preventionTips":["Count ⟪ and ⟫ are equal in every operation before submitting","Never emit a bare ⟫ in pattern text unless closing an opened selection","Regenerate the whole payload rather than hand-patching markers"],"tags":["sloppy","selection-markers","payload-validation"],"backgroundTag":"unbalanced-selection-markers","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}