fix(engine): 回應太大就說「回應太大」——不再冒充「請求失敗」(Arcrun#92)

真因:零件(main.go)給 host function 的接收緩衝區是固定大小(http_request 64KB、
claude_api 1MB)。回應超過這個大小時,wasi-shim 的 writeOut 照寫不誤:

    new Uint8Array(buf, outPtr, data.length).set(data)

data 比零件的 outBuf 大 → 覆寫零件堆積體,零件接著 outBuf[:outLen] 切片 panic;
或 writeOut 撞 memory 邊界丟例外 → 回 1 → 零件印一句 "HTTP request failed"。
使用者照那句去查連線/URL/防火牆,方向全錯。

修法(容量握手,不改 host function 簽名、向後相容):
- 零件呼叫前把 outBuf 長度預先寫進 *outLenPtr(宣告容量)
- host 在寫回前讀這個值當上限;塞不下就**不寫**(不再覆寫零件記憶體),回新的
  HOST_TOO_LARGE=3
- http_request host fn 收到 3 → 改寫一段講真話的 error envelope(實際大小+上限+
  「不是連線失敗」+分頁/篩選的具體做法+機器可讀 code/actual_bytes/limit_bytes),
  沿用既有 parsed["error"] 判定鏈原樣送到使用者面前
- 舊零件沒宣告容量(讀到 0)→ 維持舊行為。不可硬套 64KB 預設:各零件緩衝區大小不同,
  硬套會把原本正常的大回應誤判成「太大」,那只是換一種說謊

同一條路徑上另一個「訊息與真因脫節」一併修:component-loader 的 makeHttpRunner
`try res.json() catch res.text()`,在零件回非 JSON 時 body 已被消費 → 丟
"Body has already been used",與真因無關(同檔 readBodyOnce 的註解早就寫明這個坑)。
改成只讀一次。

驗證狀態(誠實標示,mindset §7):
- 通:5 顆零件 tinygo build 全過,wasm 已重編進 .component-builds/
  (claude_api 依 .gitignore 慣例不入庫,由部署端重編)
- 未跑:runtime 驗證。本 session 的權限層擋掉 node/vitest/wasmtime,
  before/after 實測輸出待人跑 scripts/repro-oversize-response.mjs
- 未做:.worker-builds/ 重編(需 node scripts/build-worker-artifacts.mjs),
  否則修法不會進 self-hosted 安裝路徑(Arcrun#93 同款陷阱)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-12 16:49:22 +08:00
parent a24f2912eb
commit 8e9bd09072
14 changed files with 389 additions and 40 deletions
+17 -2
View File
@@ -15,9 +15,14 @@ import (
"encoding/json"
"io"
"os"
"strconv"
"unsafe"
)
// 與 cypher-executor/src/lib/wasi-shim.ts 的 HOST_* 同一組回傳碼。
// 3 = 資料塞不進零件宣告的接收緩衝區(Arcrun#92)
const hostTooLarge uint32 = 3
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
@@ -102,7 +107,9 @@ func main() {
methodBytes := []byte("POST")
outBuf := make([]byte, 1024*1024) // 1MB
var outLen uint32
// 容量握手(Arcrun#92):先把緩衝區大小告訴 host,塞不下時 host 會回一段
// 講明「回應太大 + 實際/上限大小 + 該怎麼辦」的 envelope,而不是硬寫爆記憶體。
outLen := uint32(len(outBuf))
urlPtr, urlLen := safePtr(urlBytes)
methodPtr, methodLen := safePtr(methodBytes)
@@ -117,8 +124,16 @@ func main() {
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
// 回傳碼與 wasi-shim.ts 的 HOST_* 同一組:0=成功 1=引擎端錯誤 3=回應塞不下緩衝區
if result == hostTooLarge {
writeError("Mira 的回應太大,裝不下:超過這個零件單次能接收的 1 MB 上限。" +
"這不是連線失敗,也不是 Mira 沒回應。" +
"做法:把 prompt 改成請 Mira 回短一點(或分段回),或改用 callback_url 走非同步取回。")
return
}
if result != 0 {
writeError("Mira daemon request failed (host_http_request returned non-zero)")
writeError("沒有拿到 Mira 的回應:引擎的 host function 回傳錯誤碼 " + strconv.Itoa(int(result)) +
"(0=成功 1=引擎端錯誤 3=回應太大)。這是引擎側的問題,不是 prompt 寫錯。")
return
}