fix(review): stop multi-agent runs wedging on "Finalizing output..." · Entire

fix(review): stop multi-agent runs wedging on "Finalizing output..."

2b104c0→main·

dipree·2w ago·9 files·+177 added/-64 removed

A long-running worker can emit multi-MB of narrative markdown. The post-run DumpSink rendered each worker's narrative through glamour on the orchestrator's finalize goroutine, and glamour's cost is strongly super-linear (~6s at 2MB, ~49s at 4MB, minutes-to-never beyond). So the whole run hung on a frozen "Finalizing output..." with the TUI never exiting.

Worker output is raw material, not a deliverable: the human reads the styled final report and can drill into a worker's buffer interactively. So DumpSink now writes plain markdown directly and never calls glamour.

Also:

Sessions

e32caf7f5d7dView transcript

Changes

9

27 unmodified lines

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
3 unmodified lines

47
48
49
50
51
52
53
54
55
56
57
58
59

27 unmodified lines

// is available. Matches the cap used by status_style.getTerminalWidth.
const DefaultTerminalWidth = 80

// MaxRenderBytes caps the markdown size handed to glamour. Glamour's render
// cost is strongly super-linear: ~1s at 500KB, ~6s at 2MB, ~49s at 4MB, and
// minutes-to-effectively-never beyond that. A long-running agent can emit
// multi-MB narratives, and review's post-run DumpSink renders each one on the
// orchestrator's finalize goroutine — so an unbounded render wedges the whole
// run on a frozen "Finalizing output..." with no way forward. Above this cap
// we skip styling and return the raw markdown unchanged: still fully readable,
// just unstyled, and bounded. 256KB renders in well under a second.
const MaxRenderBytes = 256 * 1024

// Render produces a glamour-styled string from markdown using the entire
// CLI palette. width is the word-wrap target; darkBackground selects the
// dark or light palette variant.
3 unmodified lines

// than a runtime condition. Renderer panics are recovered and returned as
// errors so callers can fall back to raw markdown instead of crashing.
func Render(markdown string, width int, darkBackground bool) (rendered string, err error) {
    // Guard against glamour's super-linear blowup on very large inputs: above
    // MaxRenderBytes, return the raw markdown unchanged rather than hang the
    // caller for minutes. See MaxRenderBytes for the cost curve.
    if len(markdown) > MaxRenderBytes {
        return markdown, nil
    }

defer func() {
        if r := recover(); r != nil {
            rendered = ""

Mcmd/entire/cli/mdrender/mdrender.go+17

3 unmodified lines

4
5
6
7
8
9
10
99 unmodified lines

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

3 unmodified lines

"bytes"
    "strings"
    "testing"
    "time"

"github.com/entireio/cli/cmd/entire/cli/mdrender"
)

// TestRender_EmptyInputDoesNotPanic verifies the renderer handles edge cases // (empty string, whitespace-only) without erroring. // TestRender_OversizedInputReturnsRawQuickly pins the guard against glamour's // super-linear blowup: inputs larger than MaxRenderBytes must skip styling and // return the raw markdown unchanged (and near-instantly) instead of wedging the // caller for minutes. Regression for the review run that hung forever on // "Finalizing output..." while DumpSink rendered a multi-MB agent narrative. func TestRender_OversizedInputReturnsRawQuickly(t *testing.T) { t.Parallel()

// 8MB of markdown took >4 minutes through glamour in benchmarking; the // guard must make this effectively free. big := strings.Repeat("# Heading\n\nparagraph text here\n\n", (810241024)/30) if len(big) <= mdrender.MaxRenderBytes { t.Fatalf("setup: test input %d should exceed MaxRenderBytes %d", len(big), mdrender.MaxRenderBytes) }

done := make(chan struct{}) var out string var err error go func() { out, err = mdrender.Render(big, 80, true) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("Render did not return within 5s for oversized input — size guard missing") }

if err != nil { t.Fatalf("Render: %v", err) } if out != big { t.Errorf("oversized input should be returned raw and unchanged (len in=%d out=%d)", len(big), len(out)) } if strings.Contains(out, "\x1b[") { t.Error("oversized input should not be glamour-styled") } }

func TestRender_EmptyInputDoesNotPanic(t *testing.T) { t.Parallel()