fix(output): unify sanitize/budget layer across Discord, TUI, CLI, and agents (salvage #758+#775+#782) - #787
Merged
Merged
Conversation
…gents Salvaged from draft PRs #758, #775, #782 (all abandoned mid-loop). - support/outputBudget.ts: destination-specific field/message/aggregate limits plus truncate/capArray/paginateEmbedFields/genericUserError helpers. - discord/embedUtils.ts: EMBED_LIMITS + safeAddField/safeSetTitle/ safeSetDescription/safeSetFooter/enforceAggregateBudget/truncateField. - tui/sanitize.ts: sanitizeAndBoundTerminalText, sanitizeAndNeutralize, clampAndSanitize, escapeHtml, formatMonitorError. - codexResponses: incremental createResponsesReducer (no event-history retention) + MAX_FRAME_LENGTH/MAX_REASONING_BUF stream bounds. - discordCore: allowedMentions parse:[] on every reply/send. - Wired: discordHandlers, discordPair, cliRunner, pipelineFormat, tester, reviewer, skillDocumenter, workerAuditLog, workflowLinear, AuditBoard, DataTable, ChatLog, MonitorPanel, en/ko prompts.
This was referenced Sep 28, 2026
Merged
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.
Salvage of draft PRs #758, #775, #782 into one coherent sanitize/budget layer.
All three drafts were abandoned by the broken autonomous loop (172–247 commits
behind, bodies saying "failed 4 times"). They overlap on
discordHandlers.ts,tui/sanitize.ts, andcliRunner.ts, so they are reconciled here into a singlelayer instead of three competing ones.
Kept
src/support/outputBudget.ts(new) — from #782. Destination-specificfield/message/aggregate limits (Discord embed per-field + 6000 aggregate +
1900 message, Linear, prompt, audit, TUI, CLI, pipeline) and the helpers
truncate/truncateWithSuffix/capArray/codeList/boundedFieldValue/boundedDescription/boundedMessageContent/boundedLinearText/flattenToSingleLine/sanitizeException/genericUserError/paginateEmbedFields. Wired into 9 consumers:pipelineFormat,discordHandlers,cliRunner,tester,workerAuditLog,workflowLinear,AuditBoard,DataTable, plusreviewer/skillDocumenter.src/discord/embedUtils.ts(new) — from #758.EMBED_LIMITSplussafeAddField/safeSetTitle/safeSetDescription/safeSetFooter/enforceAggregateBudget/truncateField. The limits are re-exported fromoutputBudget.tsrather than restated, so the embed layer and thedestination-agnostic helpers cannot drift apart.
src/tui/sanitize.ts— #758 and #775 reconciled. AddssanitizeAndBoundTerminalText+MAX_RENDERED_LINE_LENGTH/MAX_TOTAL_RENDERED_CONTENT(#758),escapeHtml/formatMonitorError(#758), and
sanitizeAndNeutralize/clampAndSanitize(#775).sanitizeAndNeutralizenow delegates its control-character pass tosanitizeTerminalTextinstead of a second control-character regex.src/adapters/codexResponses.ts— #775'screateResponsesReducer, abounded incremental reducer that replaces
events.push(ev)+reduceResponsesEvents(events)on the live stream path, so the full parsedevent history is never retained.
reduceResponsesEventsremains as a thinwrapper for the batch/test path. Folds in #762's
MAX_FRAME_LENGTHandMAX_REASONING_BUFframe bounds from the streaming-bounds group.src/discord/discordCore.ts— #775'swithDisabledMentions/allowedMentions: { parse: [] }applied to every reply/send/thread.send, soexternally derived content cannot trigger mention parsing.
#758 render-path wiring —
discordHandlers,discordPair,cliRunner,ChatLog,MonitorPanel, and the en/ko prompt catalogs (evidence bounding,aggregate feedback cap,
MAX_FEEDBACK_ITEMS).Dropped
discordHandlers.tsrewrite — as its triage warned, it rewrote thefile from 996 to 611 lines, deleting
handleLimits/handleAuto/handleApprove/handleRejectwhile its owndiscordCore.tsstill importsand calls them. That does not compile and removes the human-approval
(
!approve/!reject) gate. Kept the current handlers and applied only thesanitize/mention/reducer hunks.
handleDevProgress.test.tsrewrite — it asserted bounded genericreplies for the handler it deleted. The gate survives here, so main's
expectations stay.
dashboardHtml.tschange — it added a second exportedescapeHtmlwith a different escaping table than main's existing in-template
escapeHtml/escapeAttrpair, and nothing imported it. Main's fix(support-output): safely render dashboard values and decode Git streams — prevent injection and corrupted repository state #770 mergealready hardened dashboard rendering; kept main's version.
.copy-verify-sizes.mjs,.line-lens-cliRunner.txt,.size-calc-cliRunner.txt,.size-measure-cliRunner-copy.ts,ls,ls-run-verify.sh,ls-verify-agt3465,scripts/agt3465-verify-once.sh,src/.agt3465-probe.txt,src/agt3465-run-once.cjs.package.json/package-lock.jsonchurn: fix(output): sanitize and budget rendered content — prevent prompt injection, invalid embeds, terminal corruption, and HTML injection #758 and fix(output): enforce aggregate output budgets across agents, Discord, runners, and TUI — prevent payload rejection and rendering abuse #782 each re-pinnedvitest (^4.0.18 → ^4.1.8 / ^4.1.11) and fix(output): enforce aggregate output budgets across agents, Discord, runners, and TUI — prevent payload rejection and rendering abuse #782 re-added
@vitest/*devDeps.No dependency is needed by this change, so no lock regeneration was required.
Conflicts resolved
tui/sanitize.ts— fix(output): sanitize and budget rendered content — prevent prompt injection, invalid embeds, terminal corruption, and HTML injection #758 and fix(external-integrations): bound and sanitize streamed and rendered provider content — prevent memory growth and untrusted-output exposure #775 both append to the same file. Merged into onemodule with a single control-character implementation.
discordHandlers.ts— fix(output): sanitize and budget rendered content — prevent prompt injection, invalid embeds, terminal corruption, and HTML injection #758's embed-utils rewrite is the base; fix(output): enforce aggregate output budgets across agents, Discord, runners, and TUI — prevent payload rejection and rendering abuse #782'sbudget pagination (
paginateEmbedFieldsreplaces fixed 10-per-page paging,which sized pages by item count and could still blow the 6000-char ceiling)
and
genericUserErrorerror paths were layered on top. All four handlersincluding
handleApprove/handleRejectare intact.cliRunner.ts— fix(output): sanitize and budget rendered content — prevent prompt injection, invalid embeds, terminal corruption, and HTML injection #758's line/total bounds and fix(output): enforce aggregate output budgets across agents, Discord, runners, and TUI — prevent payload rejection and rendering abuse #782'ssanitizeException/flattenToSingleLine/truncatebudgets combined with fix(streaming): bound event and subprocess output retention — unbounded clients and frames can exhaust process memory #762's raw-inputMAX_LINE_CHARSceiling so bounding precedes the sanitizer scan.codexResponses.ts— fix(external-integrations): bound and sanitize streamed and rendered provider content — prevent memory growth and untrusted-output exposure #775's incremental reducer kept while main's newerresponse.incomplete/response.failedterminal-error propagation(added after the draft's merge-base) is preserved inside the same
handlecallback. Both intents apply, so both sides were combined.
Verification
npx tsc --noEmit→ clean.npx oxlinton the touched files → 0 warnings, 0 errors.npx vitest run src/discord/ src/tui/ src/support/outputBudget.test.ts src/support/workflowLinear*.test.ts src/runners/ src/adapters/codexResponses.test.ts src/agents/{pipelineFormat,tester,reviewer,skillDocumenter} src/automation/workerAuditLog src/locale/→ 47 files passed, 512 passed | 1 skipped.
outputBudget.test.tsis new (13 cases);sanitize.test.tsgains 18 casescovering the merged surface.
Two tests from the drafts were rewritten rather than carried over: one asserted
a behaviour the drafts' own code did not have (
sanitizeTerminalTextona\x00b\x01c\x07d\ne\tfyieldsabcd\ne\tf, notab\nd\ne\tf), and thecodexResponses "bounded memory" case compared two ~800 KB heap deltas, which
measures GC timing rather than retention; it now asserts the streaming and batch
paths agree on a 10k-event stream.
Original drafts
Supersedes #758, #775, #782. Close them once this lands.