chore(builds): 重編執行檔——把 wait 的修法真的放進引擎成品(Arcrun#93 的閘抓到的)

#101 併進 main 後,.worker-builds/arcrun-cypher-executor 還記著 a5e4caf,
比源碼 HEAD 少了 f1370e2(wait 搬回引擎那筆)。

⇒ 這正是 #93 那道閘存在的理由:**沒有它,這次出貨會送出一個
「引擎裡沒有 wait 修法」的成品**——leo 更新完照樣燒 CPU,而出貨線全綠。
閘寫出來的隔天就抓到一次,抓到的還是我。

arcrun-cypher-executor  source a5e4caff1370e22  sha256=8411ed59b7ad

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-12 15:30:26 +08:00
parent 1791ffa497
commit a24f2912eb
2 changed files with 61 additions and 7 deletions
@@ -2650,7 +2650,7 @@ var init_recipes = __esm({
}); });
// cypher-executor/src/lib/constants.ts // cypher-executor/src/lib/constants.ts
var VALID_EDGE_TYPES, SEMANTIC_EDGE_MAP, BUILTIN_COMPONENTS; var VALID_EDGE_TYPES, SEMANTIC_EDGE_MAP, WAIT_MAX_MS, BUILTIN_COMPONENTS;
var init_constants3 = __esm({ var init_constants3 = __esm({
"cypher-executor/src/lib/constants.ts"() { "cypher-executor/src/lib/constants.ts"() {
"use strict"; "use strict";
@@ -2698,6 +2698,7 @@ var init_constants3 = __esm({
"CLICK": "ON_CLICK", "CLICK": "ON_CLICK",
"SUBFLOW": "CALLS_SUBFLOW" "SUBFLOW": "CALLS_SUBFLOW"
}; };
WAIT_MAX_MS = 3e4;
BUILTIN_COMPONENTS = /* @__PURE__ */ new Map([ BUILTIN_COMPONENTS = /* @__PURE__ */ new Map([
["comp_passthrough", (ctx) => ctx], ["comp_passthrough", (ctx) => ctx],
["comp_uppercase", (ctx) => { ["comp_uppercase", (ctx) => {
@@ -2707,6 +2708,54 @@ var init_constants3 = __esm({
["comp_counter", (ctx) => { ["comp_counter", (ctx) => {
const c = ctx; const c = ctx;
return { ...c, count: (Number(c.count) || 0) + 1 }; return { ...c, count: (Number(c.count) || 0) + 1 };
}],
// ── wait:等待 N 毫秒後繼續(Arcrun#1012026-08-12)────────────────────────
//
// 為什麼「等待」搬進引擎,而不是修那顆 WASM:
//
// 舊實作是 registry/components/wait/main.goTinyGo → WASM),用 time.Sleep。
// TinyGo 的 sleep 走 WASI `poll_oneoff`;而每顆 component worker 的 WASI shim 把
// poll_oneoff 實作成 ENOSYS`.component-builds/*/src/index.ts``poll_oneoff: () => 76`
// ⇒ TinyGo 排程器拿不到「睡到某個時間」的手段,退化成迴圈重讀 `clock_time_get`
// 自旋等時間到(wasm 內可見 runtime.sleepTicks / sleepQueue / runtime.ticks 符號)。
//
// 🔴 到這裡為止是**查得到原始碼的事實**。再往下「所以那個自旋迴圈的結束條件永遠
// 不成立」曾被當成結論寫在這裡,但**寫了測試去證,反而被打臉**:在
// vitest-pool-workers 的 workerd 裡,同步自旋 2553 圈之後 Date.now() 就前進了
// ⇒ 時鐘並沒有全程凍結。
// ⇒ 「為什麼三秒的等待會拖到 35 秒才死」的完整機制**目前仍是推測**,
// 證據只有下面 leo 的四次實測。別把它當定論往外傳。
//
// 所以症狀不是「等 N 秒花 N 秒 CPU」,而是「不管 ms 填多少都跑到 CPU 上限被砍」。
// leo 2026-08-12 在 youlin stage 實測(只有 input >> wait 兩個節點):
// ms=3000 → 38.9s 後 503 / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
// 四個值同一個死法、與 ms 無關 —— 3 秒的等待撐到 35 秒才死,就是「迴圈根本沒結束」
// 的證據(若成本與時長成正比,ms=3000 只會花 3 秒 CPU,根本不該死)。
// 也就是說 wait 零件在 Workers 上從來沒有真的等待成功過,不只是貴。
//
// 純 WASI 沙箱(stdin→stdout、無 socket、同步呼叫)本來就沒有「不花 CPU 地等」這種
// 東西 —— 會等的只有宿主。故 wait 與 trigger_workflow 同類:**是 orchestrator 的
// 執行排程職責,不是業務邏輯**(rule 02 §2.3 明列「workflow 執行排程」屬 cypher-executor
// 合法職責;§2.2 禁的是解密/簽章/template 展開/具體 API 呼叫,等待都不是)。
// 搬進引擎不違反「業務邏輯走 WASM」鐵律。引擎這側 await 一個 timer 只花 wall-clock、
// 不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。
//
// I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改:
// 吃 ms(必填 > 0)+可選 contextms > WAIT_MAX_MS 截斷;
// 回 { success: true, data: { ...context, waited_ms } }ms <= 0 回 success:false。
// 唯一刻意的放寬:ms 允許數字字串("3000")。WASM 版 json.Unmarshal 進 int 會直接
// 失敗,但 node.data 走 interpolateData 後 `ms: "{{input.delay}}"` 必然是字串
// ⇒ 收字串只會把「本來就跑不動的」變成跑得動,不會改變任何既有成功案例的行為。
["wait", async (ctx) => {
const c = ctx && typeof ctx === "object" ? ctx : {};
const requested = typeof c.ms === "number" ? c.ms : Number(c.ms);
if (!Number.isFinite(requested) || requested <= 0) {
return { success: false, error: "ms \u5FC5\u9808\u5927\u65BC 0" };
}
const ms = Math.min(Math.floor(requested), WAIT_MAX_MS);
await new Promise((resolve) => setTimeout(resolve, ms));
const passthrough = c.context && typeof c.context === "object" && !Array.isArray(c.context) ? c.context : {};
return { success: true, data: { ...passthrough, waited_ms: ms } };
}] }]
]); ]);
} }
@@ -3060,7 +3109,12 @@ var init_component_loader = __esm({
filter: "SVC_FILTER", filter: "SVC_FILTER",
merge: "SVC_MERGE", merge: "SVC_MERGE",
try_catch: "SVC_TRY_CATCH", try_catch: "SVC_TRY_CATCH",
wait: "SVC_WAIT", // wait 已於 Arcrun#1012026-08-12)移進 BUILTIN_COMPONENTSstep 1)——
// 等待是 orchestrator 的排程職責,WASI 沙箱裡做不到「不花 CPU 地等」。理由全文見
// constants.ts 的 wait 註解。這裡刻意**移除**而非留著:step 1 本來就先於 step 5 命中,
// 留下這行只會讓讀者以為 wait 還走 SVC_WAIT(實際永遠走不到)=誤導人的死路由。
// wrangler.toml 的 SVC_WAIT binding 不動(rule 3.113 個既有 binding 保留不新增),
// 拆綁定要重新部署、與本票無關。
set: "SVC_SET", set: "SVC_SET",
array_ops: "SVC_ARRAY_OPS", array_ops: "SVC_ARRAY_OPS",
string_ops: "SVC_STRING_OPS", string_ops: "SVC_STRING_OPS",
+5 -5
View File
@@ -1,18 +1,18 @@
{ {
"schema": 1, "schema": 1,
"built_for": "arcrun-tier2-worker-artifacts", "built_for": "arcrun-tier2-worker-artifacts",
"generated_at": "2026-08-12T05:36:26.216Z", "generated_at": "2026-08-12T07:29:58.626Z",
"repo_head": "cbeddf753537fbf836e20b7efce5b47b50f30d06", "repo_head": "1791ffa4972b4135dacd4208e805f67b747479c4",
"repo_dirty": false, "repo_dirty": false,
"workers": [ "workers": [
{ {
"name": "arcrun-cypher-executor", "name": "arcrun-cypher-executor",
"source_dir": "cypher-executor", "source_dir": "cypher-executor",
"source_commit": "a5e4caf5cb38c376f892708d8a46ad96a9cc23cc", "source_commit": "f1370e2275eea62b64a88821a096f2c2cfe76fb0",
"main_module": "worker.mjs", "main_module": "worker.mjs",
"main_file": "arcrun-cypher-executor/worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs",
"js_bytes": 572842, "js_bytes": 577374,
"content_sha256": "d1765930ce157de07400dcff21d620a3b5549e0a66f0d6428cc6901631ba73b2", "content_sha256": "8411ed59b7ad9e1a74ac0d8e3b620d7166e7d0178ac5939e6cc736f2e8d1d2be",
"modules": [], "modules": [],
"compat_date": "2025-02-19", "compat_date": "2025-02-19",
"compat_flags": [ "compat_flags": [