Skip to content

fix(orchestrator): sanitize sub-agent completion relay — swarm synthesis raw finalText, exec-record JSON, unborn-HEAD change-set (#11578) - #11605

Merged
0xSolace merged 1 commit into
developfrom
sol/11578-narrative-relay
Jul 2, 2026
Merged

fix(orchestrator): sanitize sub-agent completion relay — swarm synthesis raw finalText, exec-record JSON, unborn-HEAD change-set (#11578)#11605
0xSolace merged 1 commit into
developfrom
sol/11578-narrative-relay

Conversation

@0xSolace

@0xSolace 0xSolace commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes the coding sub-agent (codex ACP) transcript leak into Discord: [tool output: …] envelope blocks, raw exec-record JSON, and file bodies reaching the user. Two prior attempts took a weaker path; this fixes all three confirmed root causes with tests.

Leak paths fixed

  • A — swarm synthesis posted raw finalText verbatim (the round-3 leak). maybeFireSwarmComplete derived completionSummary from the ACP turn's response (finalText), which contains the orchestrator's own [tool output: …] envelope blocks appended by captureTerminalToolOutput. buildTaskResultLinerouteSynthesisToConnector → Discord relayed it with no stripping, unlike the sub-agent-router twin. Fix: extracted the router's private stripToolTranscript into a shared services/transcript-sanitizer.ts (hardened against empty-title [tool output: ""], unterminated/dangling blocks, and multiple blocks, plus a elideLongBlocks hard cap ≈2000 chars). Sanitize at the source in the coordinator, and again defense-in-depth in buildTaskResultLine (evidence URLs preserved). The router re-uses the shared stripper — behavior unchanged, its tests stay green.
  • B — exec-record JSON stringify. normalizeToolOutput fell back to JSON.stringify for codex exec records (call_id, command, exit_code, …), dumping the raw record into an envelope. Fix: detect the exec-record shape (has call_id and command) and render a one-liner $ <command> → exit <code> (+ capped stdout/stderr tail); never stringify a record carrying call_id.
  • C — change-set capture failed on unborn HEAD. captureChangeSet threw on git diff HEAD in a repo with zero commits → caller caught → weak narration path (why rounds 1/2 leaked). Fix: diff against the empty-tree hash on unborn HEAD, and — unborn HEAD only, to preserve the born-HEAD shared-workspace clutter invariant — merge git ls-files --others --exclude-standard so shell-written scaffolding produces a change set.

Follow-up (not in this PR)

The synthesis path double-posts alongside the planner's own clean reply (live round-3: user got the raw-envelope message and a clean "done ✅" for one task). That's a router-vs-synthesis ownership design question, deliberately left out of this fix.

Tests (+27, all green; no regressions vs baseline)

  • sanitizer: normal / empty-title / multi-block / unterminated envelopes; preserves prose + URLs; elides >2000-char remnant
  • maybeFireSwarmComplete: envelope-laden response → clean completionSummary; tool-only response → "Task completed." fallback
  • buildTaskResultLine (via handleSwarmSynthesis): envelopes stripped, evidence URL preserved
  • normalizeToolOutput: exec-record → one-liner (array/string command, capped tail, stringified record, non-record untouched)
  • captureChangeSet: unborn HEAD + shell-written files → change set; untracked included only on unborn HEAD; born-HEAD clutter invariant preserved

Orchestrator suite: 996 passing (was 969; +27 mine). The 15 pre-existing failures are unrelated environment/dependency issues (drizzle-orm, @noble/curves, i18n codegen) — identical count with and without this change.

[sol-relay] — [sol-orch]

…sis raw finalText, exec-record JSON, unborn-HEAD change-set (#11578)

Three leak paths let coding sub-agent (codex ACP) transcript reach Discord:

A) Swarm synthesis posted the ACP turn finalText VERBATIM. That finalText
   contains the orchestrator's own [tool output: ...] envelope blocks, and
   the synthesis path (swarm-coordinator maybeFireSwarmComplete ->
   server-helpers-swarm buildTaskResultLine -> connector) had NO stripping,
   unlike the sub-agent-router twin. Fix: extract the router's transcript
   stripper into a shared services/transcript-sanitizer.ts (hardened for
   empty-title, unterminated, and multi-block envelopes + a length cap),
   sanitize completionSummary at the SOURCE in the coordinator, and again
   defense-in-depth in buildTaskResultLine (evidence URLs preserved). Router
   behavior is unchanged (re-uses the shared stripper; its tests stay green).

B) normalizeToolOutput JSON.stringify'd codex exec records (call_id, command,
   exit_code, ...) into envelopes. Fix: detect the exec-record shape and render
   a compact `$ <command> -> exit <code>` one-liner (+ capped stdout/stderr
   tail); never stringify a record carrying call_id.

C) captureChangeSet threw on an unborn HEAD (git diff HEAD with zero commits),
   so the caller fell back to the weak narration path. Fix: diff against the
   empty-tree hash on unborn HEAD, and (unborn HEAD only, to preserve the
   born-HEAD shared-workspace clutter invariant) merge untracked files so
   shell-written scaffolding produces a change set.

Follow-up (not in this PR): the synthesis path DOUBLE-POSTS alongside the
planner's own clean reply — a router-vs-synthesis ownership design question.

Tests: +27 (sanitizer robustness, coordinator source sanitize + default
fallback, buildTaskResultLine strip + URL preservation, exec-record one-liner,
unborn-HEAD/untracked change-set). All green; no regressions vs baseline.

Co-authored-by: wakesync <shadow@shad0w.xyz>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34770391-d3b0-46cf-9d8a-6286b5d6e04a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sol/11578-narrative-relay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@0xSolace
0xSolace merged commit 3781312 into develop Jul 2, 2026
34 of 57 checks passed
@0xSolace
0xSolace deleted the sol/11578-narrative-relay branch July 2, 2026 21:35
lalalune pushed a commit that referenced this pull request Jul 3, 2026
…nt files

the unborn-HEAD change-set scoop (3781312, #11605) unions
'ls-files --others --exclude-standard' into changedFiles, but a fresh
scaffold that runs npm install BEFORE writing .gitignore has thousands
of untracked node_modules paths (--exclude-standard has nothing to
honor yet). those flooded the 60-file cap, and because agent-written
tool paths were spread LAST (Set dedupe keeps first occurrence), the
flood evicted the agent's real files: 'what did you change' answered
with node_modules noise and diffs rendered junk.

- filter vendor/build dirs (node_modules, .venv, dist, ...) from the
  unborn-HEAD untracked scoop only; born-HEAD never scoops untracked
  and explicit tool-written paths are always kept
- spread agentWritten first so explicit edit/write tool calls survive
  the MAX_CHANGED_FILES cap
- drop the truncated garbage tail line when the ls-files listing was
  cut at maxBuffer (ENOBUFS)
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant