Repository navigation
fix(runner): 输出分页不再跳过未交付的字节 - #498
Merged
Merged
Conversation
Qiyuanqiii
added a commit
that referenced
this pull request
Oct 27, 2026
commandRuntimeRing.read backed a page off inside a multi-byte rune to keep the returned text valid UTF-8, then kept looping. The next frame starts past that residue, and neither loop guard rejects a frame whose start is ahead of the resume cursor, so the page delivered it in full and published a NextCursor over bytes it never returned. A 6-byte page over two frames claimed [0,23) while delivering 6 bytes, dropping 17. NextCursor is the durable resume cursor for the operator drain, the thread activity reader and receipt-based resumes, so the skipped bytes were reported as already delivered. End the page where the bound trimmed a frame and let the next page continue through the normal in-frame cut path.
Qiyuanqiii
force-pushed
the
Qiyuanqiii/runtime-page-cursor
branch
from
October 27, 2026 17:51
4b75e52 to
fd374db
Compare
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.
commandRuntimeRing.read在页预算落在一个多字节 rune 中间时,把这一帧的文本回退到完整 rune 再交付(internal/runner/command_runtime_job.go:1812-1819:data = data[:remaining],然后for len(data) > 0 && !utf8.Valid(data) { data = data[:len(data)-1] },copyFrame.NextCursor = copyFrame.Cursor + uint64(len(data)))。回退之后remaining还剩 1–3 字节(used只按实际交付的字节扣,:1824-1825),于是循环继续走到下一帧。而下一帧的Cursor已经落在当前cursor之后:循环头的两个守卫都拦不住它——:1795只拦frame.NextCursor <= cursor(整帧已交付)与remaining <= 0,:1799只在cursor > copyFrame.Cursor(游标落在帧内部)时做 cut。起点在游标之后的帧因此被整帧从头交付,并把page.NextCursor推到它的末尾(:1826):中间那段字节从没返回给调用方,却已经被恢复游标声明为"已交付"。实测(ring 级两条 append:第一帧
😀😀😀😀😀\n21 字节,第二帧x\n2 字节,6 字节页预算),修复前:一页声明覆盖
[0,23)却只返回 6 字节,17 字节被跳过;Wait这一层(公开 API)同样复现,说明丢失不在测试夹具里,而在分页契约上。只有多字节输出会触发:纯 ASCII 下回退循环一步都不走,remaining归 0,下一轮直接被remaining <= 0拦住,行为与现在完全一致。命令输出里 CJK 文本与框线进度条是常态,所以这条不需要畸形输入。NextCursor就是那个持久恢复游标,丢掉的字节不会补回来:internal/application/command_runtime_operator.go:207-213用cursor = page.NextCursor排空输出,它的健全性检查只拒绝"游标倒退"和"越过EndCursor",对这种跳号是放行的,随后还会把这一帧之前的内容当成已读完。internal/application/thread_application_services.go:269把page.NextCursor原样投影成线程活动的 HTTP 游标,调用方按它续读。internal/runner/command_runtime_job.go:1737-1743),所以一次跳号最多吞掉接近一整帧的保留输出;页预算由调用方给(internal/toolgateway/command_runtime.go:83-85限定在[MinCommandRuntimeOutputRead, MaxCommandRuntimePageBytes],而MinCommandRuntimeOutputRead = utf8.UTFMax,即 4 字节也合法),内联窗口最大 512 KiB(internal/runner/command_runtime.go:39)——小max_bytes轮询是正常用法,不是极端参数。修法取最窄的条件:只有当预算真的把一帧截断(发生了回退式截断)时,就在交付这一帧后结束本页,未完成的那一帧留给下一页;下一页进来时游标落在该帧内部,走既有的 cut 分支(
:1799-1810)继续。copyFrame.Text == ""时原有的break(:1820)保持不变,所以"预算装不下一个字符"的既有行为不受影响。我没有改成"下一帧起点不等于游标就 break"那种写法:那会把 cut 对齐推进(:1805-1809)也变成整页空返回,而现有那条空文本break的语义我不想一起改动。验证:
go test -count=1 -timeout 380s -run "TestCommandRuntimeRingPageStaysContiguous|TestCommandRuntimeManagerPagedWaitKeepsMultiByteOutput" ./internal/runner/:修复前两条 FAIL(上面摘录),修复后ok … 0.775s。红是用git checkout --临时还原command_runtime_job.go得到的,跑完立刻恢复,没有留下临时文件。EndCursor(ring 级对照ring.next,Wait级对照EndCursor)——即修复既不能少读,也不能把截断变成停滞。go test -count=1 ./internal/runner/ ./internal/toolrun/全包通过:ok runner 9.007s、ok toolrun 0.278s。go test -count=1 -run "CommandRuntime" ./internal/application/ ./internal/httpapi/ ./internal/store/通过:ok application 198.911s、ok httpapi 0.936s、ok store 10.738s(这三包按-run取与本改动相关的用例,因为全包在本机单包会撞默认超时)。go vet ./internal/runner/通过;gofmt -l internal/runner/无输出;git diff --check干净。我没有跑go test ./...全量:internal/application、internal/store的单包全量在本机会撞 10 分钟默认超时(CI 拆 shard),全量以本 PR 的 CI 为准。ocr delegate preview --from 274fefb6 --to HEAD→ 1 个可复核文件 / 共 2 个改动文件(+133/-0),command_runtime_output_page_continuity_test.go被default_path规则排除,已人工复核(只用 ring 与公开Wait,无跳过、无放宽既有期望,夹具是合成 emoji/ASCII)。按**/*.go规则组逐条核对:Collections/Slices/Numeric——新增只有一个 bool 与一处break,切片下标仍由remaining/len(data)限界,utf8.Valid回退逻辑未动;Errors/Contracts——未新增错误路径,CommandRuntimeOutputPage字段语义不变,只是不再声明未交付的区间;Context/并发——read在 ring 自己的互斥区内(未改锁范围),无新增 goroutine 或共享状态;资源生命周期/定时器——不涉及。无发现。无 Surface 变化:不改 DTO 形状、HTTP 参数或协议版本,
docs/openapi.json与protocols/registry.json无需再生成;不触碰权限、Scope、沙箱、预算或执行判定;测试夹具为合成文本,不含凭据与本地运行数据。