{"record":{"id":"751db808ba6736ca","repo":"paperclipai/paperclip","slug":"could-not-replace-provider-conversation-boundary","errorCode":null,"errorMessage":"Could not replace ${provider} conversation boundary","messagePattern":"Could not replace (.+?) conversation boundary","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"doc/plans/chat-adapters/generate-wireframes-v7.mjs","lineNumber":201,"sourceCode":"  const end = svg.indexOf(marker, start);\n  if (start < 0 || end < 0) throw new Error(`Could not replace ${provider} ${tab} navigation`);\n  const height = Number(svg.match(/<svg[^>]+height=\"(\\d+)\"/)?.[1]);\n  return `${svg.slice(0, start)}${endpointNav(provider, tab, height)}${svg.slice(end + 4)}`;\n}\n\nconst conversationBoundaryCopy = {\n  Slack: \"A channel root and its replies are one task. The first mention in an existing thread binds it. A DM has one open task; after completion the next message starts another.\",\n  GitHub: \"An issue, pull-request conversation, or inline review thread binds once to one Paperclip task.\",\n  \"Microsoft Teams\": \"A channel post and its replies are one task. A personal or group chat has one open task; after completion the next message starts another.\",\n  Telegram: \"A DM or ordinary group has one open task; after completion the next addressed message starts another. A forum topic has one stable topic-to-task binding.\"\n};\n\nfunction patchConversationBoundary(svg, provider, mobile) {\n  const startToken = mobile ? '<text x=\"16\" y=\"260\"' : '<text x=\"504\" y=\"234\"';\n  const endToken = mobile ? '<line x1=\"16\" y1=\"348\"' : '<line x1=\"504\" y1=\"278\"';\n  const start = svg.indexOf(startToken);\n  const end = svg.indexOf(endToken, start);\n  if (start < 0 || end < 0) throw new Error(`Could not replace ${provider} conversation boundary`);\n  const copy = conversationBoundaryCopy[provider];\n  const replacement = mobile\n    ? multiline(16, 260, wrap(copy, 46, 4), 12, \"#666\", 18)\n    : multiline(504, 234, wrap(copy, 88, 2), 14, \"#666\", 20);\n  return `${svg.slice(0, start)}${replacement}${svg.slice(end)}`.replaceAll(\"active task\", \"open task\");\n}\n\nconst sharedDefinitions = [\n  { id:\"01\", slug:\"connectors-catalog\", title:\"Connectors\", subtitle:\"Connect tools and places where people talk to agents.\", group:\"Start\", tab:\"Shared\", annotations:[\"The existing Apps catalog remains the entry point.\", \"Filters separate chat and tool methods.\", \"Each connector row has one Connect action.\", \"Connection state remains visible in the catalog.\"], rationale:\"The current Connectors surface remains canonical.\" },\n  { id:\"02\", slug:\"connection-purpose\", title:\"Choose how to connect\", subtitle:\"Shown for every connector that supports both chat and tool methods.\", group:\"Start\", tab:\"Shared\", annotations:[\"The existing connection wizard shell and selected provider are reused.\", \"Chat with an agent is the incoming-conversation path.\", \"Use this connection as an agent tool is the outbound tool/credential path.\", \"Single-purpose providers skip the choice.\"], rationale:\"The registry drives the same direction choice for every dual-surface connector.\" },\n  { id:\"03\", slug:\"choose-agent\", title:\"Which agent do you want to chat with?\", subtitle:\"Choose the one agent represented by this connection.\", group:\"Start\", tab:\"Shared\", annotations:[\"The existing agent selector is reused.\", \"Only active agents can be selected.\", \"One selection is required.\", \"Continue begins provider setup.\"], rationale:\"This is the only shared Paperclip-specific setup decision.\" },\n  { id:\"11\", slug:\"bound-task\", title:\"Externally bound task\", subtitle:\"A normal Paperclip task with explicit publication and detach controls.\", group:\"Paperclip\", tab:\"Task\", annotations:[\"The task shows its external source.\", \"External actors remain attributed.\", \"Publishing back to the provider is explicit for human comments.\", \"The agent remains locked until detach.\"], rationale:\"External work stays in the ordinary governed task experience.\" },\n  { id:\"12\", slug:\"agent-channels\", title:\"Agent Channels\", subtitle:\"See every provider identity representing this agent.\", group:\"Paperclip\", tab:\"Agent\", annotations:[\"Channel identities are summarized per provider.\", \"Health and recent tasks remain visible.\", \"Connections open in Connectors.\", \"Connect a channel preselects this agent.\"], rationale:\"Agent detail summarizes endpoints while Connectors manages them.\" }\n];\n\nconst sharedScreens = sharedDefinitions.map((screen) => {\n  for (const suffix of [\"\", \"-mobile\"]) writeFileSync(join(out, `${screen.id}-${screen.slug}${suffix}.svg`), readFileSync(join(previous, `${screen.id}-${screen.slug}${suffix}.svg`), \"utf8\"));\n  return { ...screen, desktopSize:\"1280×800\", mobileSize:\"375×812\" };","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/doc/plans/chat-adapters/generate-wireframes-v7.mjs#L183-L219","documentation":"`patchConversationBoundary` rewrites the conversation-boundary explanatory copy inside each provider SVG by finding fixed coordinate anchors (`<text x=\"504\" y=\"234\"` desktop, `<text x=\"16\" y=\"260\"` mobile) and an end marker (`<line ...>`), then splicing in wrapped copy. Missing anchors mean the base SVG layout drifted from what the patcher assumes, so it throws per provider.","triggerScenarios":"Running the v7 generator when the base SVG for a provider no longer contains the boundary text/line at the expected coordinates — layout changes moved the text, the block was removed, attribute order changed (e.g. `<line y1=\"278\" x1=\"504\" ...>`), or mobile/desktop variants were restructured.","commonSituations":"Adjusting SVG spacing/typography in shared helpers so y=234/260 or the line at y1=278/348 moved; deleting the placeholder boundary block; reformatting attribute order in the SVG emitter.","solutions":["Regenerate or inspect a base SVG and update `startToken`/`endToken` in `patchConversationBoundary` to the current coordinates and attribute order.","Keep the boundary placeholder emission and the patcher anchors in one shared constant set so they cannot drift.","If attribute order changed, match on the minimal stable substring or use a regex instead of exact indexOf tokens.","Re-run the generator and verify every provider/mobile combination patches cleanly."],"exampleFix":"// before\nconst startToken = mobile ? '<text x=\"16\" y=\"260\"' : '<text x=\"504\" y=\"234\"';\n// after (anchors moved)\nconst startToken = mobile ? '<text x=\"16\" y=\"280\"' : '<text x=\"504\" y=\"254\"';","handlingStrategy":"validation","validationCode":"const hasBoundaryAnchors = (svg, mobile) => {\n  const s = mobile ? '<text x=\"16\" y=\"260\"' : '<text x=\"504\" y=\"234\"';\n  const e = mobile ? '<line x1=\"16\" y1=\"348\"' : '<line x1=\"504\" y1=\"278\"';\n  return svg.includes(s) && svg.includes(e);\n};","typeGuard":"const canPatchBoundary = (svg, mobile) => svg.indexOf(mobile ? '<text x=\"16\" y=\"260\"' : '<text x=\"504\" y=\"234\"') >= 0;","tryCatchPattern":"try {\n  svg = patchConversationBoundary(svg, provider, mobile);\n} catch (err) {\n  if (String(err.message).includes(\"conversation boundary\")) {\n    console.error(`Boundary anchors missing for ${provider}${mobile ? \" (mobile)\" : \"\"}; resync patchConversationBoundary with the SVG emitter.`);\n  } else throw err;\n}","preventionTips":["Keep boundary text/line coordinates in one constants module used by both the SVG emitter and the patcher.","Avoid reordering SVG attributes if string anchors depend on them; prefer regex anchors instead.","Run the full v7 generation across all providers and both orientations after layout changes."],"tags":["build-script","svg","string-parsing"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}