fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(mcp): capText's truncation marker could push output past budget - #654

Open
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun
Open

fix(mcp): capText's truncation marker could push output past budget#654
teyrebaz33 wants to merge 2 commits into
sapiom:mainfrom
teyrebaz33:fix/mcp-captext-budget-overrun

Conversation

@teyrebaz33

@teyrebaz33teyrebaz33 commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

execution-projection.ts documents capText (packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that — it sliced text to exactly budget characters, then appended a truncation marker AFTER the slice, so the returned string was budget + marker.length characters, not budget. Concretely, capText(text, 2000, someUrl) could return a 2094-char string.

Summary and scope

Since the marker's own length depends on the dropped-char count, which depends on where we slice, which depends on the marker's length, this resolves it with a small converging loop that shrinks the slice point until slice + marker fits within budget, with a final .slice(0, budget) backstop for degenerate tiny budgets. This also fixes the reported dropped-char count, which the old version could get slightly wrong for the same reason.

Out of scope: no changes to execution-projection.ts itself or to the field budgets it configures; no dependency changes.

Related work

Related issue or discussion: #653

Validation

# packages/mcp: vitest run / tsc --noEmit / eslint src --ext .ts
15/15 test files, 107/107 tests passing (including the new suite)
tsc --noEmit: clean
eslint: 0 errors, 0 warnings

Tests and documentation

Added packages/mcp/src/tools/shared.test.ts (no test file existed for this module before): a 240-case brute-force sweep across budgets (including 0/1/5/10, and the real PREVIEW_BUDGET/DEFAULT_FIELD_BUDGET values) confirming the fix never exceeds budget, plus a regression test verified via git stash to fail 4/6 cases against the pre-fix implementation and pass 6/6 against the fix. No user-facing documentation changes needed.

Compatibility and release impact

  • Breaking or externally visible changes: None. capText's signature and return type are unchanged; only truncated outputs near a field's budget boundary get slightly shorter (by the marker's length) to actually respect the documented bound.
  • Changeset: Added (.changeset/fix-captext-budget-overrun.md), patch bump.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

I used Claude (Anthropic) as a coding assistant throughout: it helped find the bug (including writing a small script that proved the overrun with real numbers), draft the fix and the brute-force test sweep, and run the verification commands quoted above. I reviewed and ran every command myself, read and understood the resulting diff line by line, and can explain and maintain every change in this PR.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

execution-projection.ts documents capText as a "hard char budget"
primitive: "the result is bounded for ANY argument combination". The
implementation didn't honor that -- it sliced text to exactly budget
characters, then appended a truncation marker (e.g. "...[truncated
48000 chars -- open https://... for the full value]") AFTER the slice,
so the returned string was budget + marker.length chars, not budget.
Concretely: capText(text, 2000, someUrl) could return a 2094-char
string. Verified with a 240-case brute-force sweep across budgets
(including 0/1/5/10) and text lengths that the old implementation
violated the bound in the small-budget cases exercised by
PREVIEW_BUDGET (2000).
Fix: since the marker's own length depends on the dropped-char count,
which depends on where we slice, which depends on the marker's length,
resolve this with a small converging loop that shrinks the slice point
until slice + marker fits within budget, with a final .slice(0,
budget) as a hard backstop for degenerate tiny budgets. Also fixes the
reported dropped-char count, which the old version could get slightly
wrong for the same reason.
Added shared.test.ts (no test file existed for this module) with a
regression test that fails 4/6 cases against the old implementation
(verified via git stash) and passes 6/6 against the fix.
No dependency or lockfile changes.
@github-actionsgithub-actionsBot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: medium Review size is 101–500 changed lines area: platform-tools Changes to CLI, MCP, sandbox, tools, or plugins labels Aug 17, 2026
@github-actionsgithub-actionsBot added bug Something isn't working and removed contribution: incomplete Required pull request information is incomplete or ambiguous review: manual External pull request requires maintainer review before automation labels Aug 21, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: platform-toolsChanges to CLI, MCP, sandbox, tools, or pluginsbugSomething isn't workingcontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationsize: mediumReview size is 101–500 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@teyrebaz33