頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定, Gitea private=除機敏值/build 產物/.github 外全 push。 解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。 機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.2 KiB
2026-05-13 cypher-executor multi-node chain context propagation 漏失
總耗時:約 20 分鐘 debug(找 P0 #9 過程中順帶找到) 根因:4 個 edge type 沒 spread baseCtx 給下游節點,原始 context 從第 2 節點開始消失 修法:ON_SUCCESS / ON_FAIL / IF / ON_CLICK 套用 PIPE / FOREACH 同模式
{...baseCtx, ...result}影響:任何 chain workflow 從第 2 節點開始 interpolate context key 都失敗
症狀
mira acr run wiki_synthesis(7 節點 workflow)回 Unauthorized。trace 顯示:
load_schema(節點 1)✓ 200,input 含api_key=ak_xxxload_skill(節點 2)✗ 401 Unauthorized,input 含api_key="{{api_key}}"← 模板原文未替換
推測 → 驗證
對照組:2 節點 chain 看 input keys:
| 節點 | input keys |
|---|---|
| n1 | api_key, b1, b2, block_id ✓ 全 context 在 |
| n2 | blocks, count, success, api_key, block_id ✗ b1, b2 不見,api_key 是原文 "{{api_key}}" |
n2 的 ctx 只有 n1 output spread,原始 context(b1, b2, ...)全消失。
根因
graph-executor.ts 在 outEdges 處理時,PIPE 跟 FOREACH 已有 baseCtx merge:
// PIPE (line 381-384)
const pipeContext = {
...(context as Record<string, unknown>),
...baseResult,
};
// FOREACH (line 438-444)
const baseCtx = ...;
const itemContext = {
...baseCtx,
...(result as Record<string, unknown>),
[iteratorKey]: item,
};
但 ON_SUCCESS / ON_FAIL / IF / ON_CLICK 直接傳 result:
case 'ON_SUCCESS':
result = await this.executeNode(nextNode, graph, result, ...); // ← bug
result 是上游節點的 output,沒有原始 context。下游 interpolate 找不到原始 key 就原文留下。
歷史脈絡
2026-05-07 commit e8fca33("FOREACH preserves outer context")已意識到問題並修了 FOREACH。但沒同步處理另外 4 個 edge type。今天才被 mira 7 節點 workflow 踩到。
→ 教訓:架構級修法要全 edge type 一致掃過,不只修當下踩到的。
修法
cypher-executor/src/graph-executor.ts 4 個 edge case 補:
// 改前
result = await this.executeNode(nextNode, graph, result, ...);
// 改後
const baseCtx = (typeof context === 'object' && context !== null) ? context as Record<string, unknown> : {};
const baseResult = (typeof result === 'object' && result !== null) ? result as Record<string, unknown> : {};
const mergedCtx = { ...baseCtx, ...baseResult };
result = await this.executeNode(nextNode, graph, mergedCtx, ...);
套用在 line 407 (ON_SUCCESS) / 415 (ON_FAIL) / 423 (IF) / 472 (ON_CLICK)。
驗證
acr run wiki_synthesis 7 節點 workflow 端對端跑通:
| 節點 | 修前 input | 修後 input |
|---|---|---|
| n2 (load_skill) | api_key={{api_key}} ✗ |
api_key=ak_xxx ✓ |
| n7 (emit_result) | 上游 spread only | baseCtx + 各上游 spread ✓ |
整條 16.2 秒(含 claude_api 真實 Claude 呼叫),結果 {success: true, data: {...}}。
Known Limitation
interpolateData() 只展開 node.data top-level string values,不遞迴 nested object:
# emit_result 的 values 內 {{...}} 不會展開
emit_result:
component: set
values:
text: "{{classify.data.text}}" # ← 原文傳,不展開
非阻擋 P1,SDD 待開 interpolate-nested-config。當前 workaround:直接看上游節點的 trace output。
未來怎麼避免
- 新 edge type 加進來時必須走 baseCtx merge 模式——可以抽出 helper
mergeCtxForDownstream(context, result)強制所有 caller 用,避免漏 - interpolation 邊界要有測試:寫一個 2 節點 chain 用
{{api_key}}引用原始 context 的 e2e test,CI 跑過 - trace output 要顯示 input —— 沒看 trace 內
input欄位很難看出 interpolation 失敗(目前 acr CLI 不顯示 input,只顯示 result)。CLI 應加--verbose顯示每節點 input
Reference
- 對應 SDD:
matrix/arcrun/.agents/specs/arcrun/arcrun.mdP0 #10 - 相關 commit:
e8fca332026-05-07(FOREACH 同類修法) - 受影響 SDD:
polaris/mira/.agents/specs/mira-app/tasks.md7B.3c - 同日另一個 incident:2026-05-13-cypher-outbound-522.md