feat(arcrun): add kbdb_upsert_block component for idempotent block writes

對應 mira 7B.3f:per-entity index-entry 維護需要「找有則 PATCH 沒找到 POST」,
arcrun workflow 沒 IF/branch 能力(已知限制 #1 + 新 P1 #1),用 kbdb_upsert_block
零件把分支邏輯封進零件內:GET /blocks?page_name=X → user_id filter → 找到 PATCH 沒找到 POST。
page_name 當 idempotency key,未來其他「找有則改沒則建」場景共用。

SDD:polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1
     matrix/arcrun/.agents/specs/arcrun/arcrun.md 三-A P1 #1 + 三-B 新零件加入紀錄
This commit is contained in:
2026-05-14 10:18:21 +08:00
parent 519423cb0d
commit 4e746986b4
9 changed files with 1572 additions and 2 deletions
+199 -2
View File
@@ -69,8 +69,205 @@ P0 全部清除才啟動封測。
| 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_keyacr init 自動存入 configCLI 1.0.9|
| 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token |
| 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 |
| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 |
| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx| 詳見下方專段 |
**目前狀況**P0 全部完成。Google Sheets 實際寫入可由封測者用真實 token 驗證,不阻塞啟動
**目前狀況**P0 全部解決
- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環)
- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致
兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。
---
### ✅ P0 #92026-05-13 已解決):cypher-executor outbound fetch 全失效
**完整事件報告(含誤判路徑)**[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md)
**修復方式**cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。
**改動檔案**2026-05-13):
- `cypher-executor/src/lib/component-loader.ts``wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev
- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名
- `cypher-executor/src/types.ts``Bindings``WORKER_SUBDOMAIN: string`
- `cypher-executor/wrangler.toml``[vars]``WORKER_SUBDOMAIN = "uncle6-me"`
- 5 個 component worker 在 dashboard 啟用 workers.dev URLkbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api**未來新 component 也都要開**
**驗證**cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。
**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。
---
### ✅ P0 #102026-05-13 已解決):multi-node chain context propagation 漏失
**現象**cypher binding workflow 從第 2 個節點開始,原始 input contexttop-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。
**測試重現**
```yaml
flow:
- "input >> ON_SUCCESS >> n1"
- "n1 >> ON_SUCCESS >> n2"
config:
n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" }
n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" }
context: { api_key: "ak_xxx", b1: "...", b2: "..." }
```
n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。
n2 收到的 ctx 只有 `n1.output spread`blocks/count/success/block_id),**`api_key / b1 / b2` 不見**`{{api_key}}` 原文傳到零件回 401。
**根因**`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。
**修法**`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472):
```typescript
// 改前
result = await this.executeNode(nextNode, graph, result, ...);
// 改後(同 PIPE/FOREACH 模式)
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, ...);
```
**驗證**mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。
**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。
---
### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14
P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。
#### A. interpolateData() 不遞迴 nested object
**現象**`set` / `kbdb_create_block``values: { text: "{{classify.data.text}}" }``tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。
**根因**`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。
**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`
**測試**`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }``name=world` → 全展開。
#### B. ctx 沒存上游 output 的 node id namespace
**現象**`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。
**根因**`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`
**修法**`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。
**測試**5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。
#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested
**現象**mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。
**根因 (C1)**`getIterableFromContext(result, key)` 只看當前節點 output。`result``create_paragraph` output`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。
**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets``getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`
**修法**
- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context`
- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」
**測試**mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 tripletparent_id 正確接到對應 paragraph。
#### Edge type 一致化
`propagateCtx(context, result, upstreamNodeId)` helper5 個 edge typePIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。
#### CLI validator 同步
`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。
---
### 三-A、P1 待改進(不擋封測,但 mira 已踩到)
#### P1 #1workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出)
**現象**mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`FOREACH+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。
**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。
**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。
**位置**cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。
---
### 三-B、新零件加入紀錄
| 日期 | 零件 | 動機 | 對應 SDD |
|---|---|---|---|
| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 |
---
### 原 P0 #9 調查紀錄(保留作歷史參考)
**現象**cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**
**測試矩陣**mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現):
| 路徑 | 結果 | 證明 |
|---|---|---|
| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 |
| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 |
| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 |
| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop |
| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 |
| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 |
**衍生小 bug**`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。
**影響範圍(封測啟動阻擋)**
- 任何用戶 workflow 含 outbound HTTP 都壞:
- 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄)
- 用戶要存資料進 arcrun 內建 KBDB → 522mira 7B.3c 卡這裡)
- 任何 auth primitive 走獨立 Worker URL 路徑 → 522
- Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞)
- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑
**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod**
調查路徑(按時序):
1. 一開始懷疑 free tier CPU cap10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻**
2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻**
3. 用戶補繳費恢復 Workers Paid → 重測仍全 522**徹底排除付費假設**
4. 看 dashboard Version History5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`**
5. 對照 GitHub Actions4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`
6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含:
- `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon`
- `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)`
- `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`
**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。
**為何 SB 路徑沒事 / HTTP 路徑全死**SB 走 cypher-executor 內部 service binding API`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。
**驗證 wrangler tail 證據**trigger 任何 outbound HTTP 的 graphcypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。
**解法(三選一)**
- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。**
- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。
- **C. 架構切換**mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。
**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。
**為什麼這直接擋封測**
封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。
**也直接擋 mira**[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-fwiki 合成 workflow)卡這裡。
---
@@ -84,7 +281,7 @@ P0 全部清除才啟動封測。
## 五、已知限制(封測期間不修)
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #12026-05-14 mira 用 `kbdb_upsert_block` workaround
2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值)
3. 用戶自製邏輯零件(Phase 5)封測後才實作