Appearance
Tech Debt Register
追蹤 @followup[TD-NNN] marker 對應的未解決項目。所有在 openspec/changes/**/tasks.md 裡出現的 marker 都必須在此有對應 entry,否則 spectra-archive 會被 pre-archive-followup-gate.sh 攔截。
規則詳見 .claude/rules/follow-up-register.md。
Index
| ID | Title | Priority | Status | Discovered | Owner |
|---|---|---|---|---|---|
| TD-001 | mcp-token-store libsql 不相容 | low | done | 2026-04-20 B16 #10 | — |
| TD-002 | guest_policy DB-direct UPDATE 造成 cache drift | mid | done | 2026-04-20 B16 #7 | — |
| TD-003 | text-dimmed 對比度不足(cross-change residual) | mid | done | 2026-04-20 B17 C#11.9 | — |
| TD-004 | 首頁 Google login button 高度 36px < WCAG 40px | high | done | B17 viewport-baseline.spec.ts | — |
| TD-005 | Admin 頁面 a11y violations 批次(@nuxt/a11y 首輪掃描) | high | done | 2026-04-21 RAF @nuxt/a11y | — |
| TD-006 | Nuxt UI subtle variant tonal badge 對比度不足 | mid | done | 2026-04-20 TD-003 e2e exclude | — |
| TD-007 | 裝飾 icon tonal color 低於 WCAG 1.4.11 non-text AA | low | done | 2026-04-20 TD-006 review | — |
| TD-008 | acceptance-tc-0x MCP 整合測試在 TD-001 修後破損 | mid | done | 2026-04-20 add-ai-gateway | — |
| TD-009 | user_profiles.email_normalized 全面改 nullable | mid | done | 2026-04-21 passkey-authentication | — |
| TD-010 | credentials / admin-members endpoint libsql 不相容 | mid | done | 2026-04-21 passkey §16 DR | — |
| TD-011 | migration 0009 FK cascade 設計不符 self-delete / audit | high | done | 2026-04-21 passkey §17.8 | — |
| TD-012 | passkey-first → link Google 被 better-auth email 檢驗擋住 | high | done | 2026-04-21 passkey §17.3 | — |
| TD-013 | /account/settings 新增 passkey 缺 naming dialog | low | done | 2026-04-21 passkey §17.2 | — |
| TD-014 | error-sanitizer 後 12 test 抛 evlog Logger not init | mid | done | 2026-04-21 drizzle-refactor apply | — |
| TD-015 | SSE 長連線缺 heartbeat,30s proxy timeout 風險 | mid | done | 2026-04-24 /commit review | — |
| TD-016 | isAbortError / createAbortError 在四處重複實作 | low | done | 2026-04-24 /commit review | — |
| TD-017 | chat.post.ts 兩個 AI binding getter 可合併 | low | done | 2026-04-24 /commit review | — |
| TD-018 | Container.vue classifyError 巢狀條件抽 lookup table | low | done | 2026-04-24 /commit review | — |
| TD-019 | SSE reader pattern 在 client/server 雷同可抽共用 | low | done | 2026-04-24 /commit review | — |
| TD-020 | CHATGPT_CONNECTOR_OAUTH_PATH_PATTERN 可收緊字元集 | low | done | 2026-04-24 /commit review | — |
| TD-021 | ConversationHistory bucket toggle 缺 aria-expanded 等 | low | done | 2026-04-24 /commit review | — |
| TD-022 | groupedConversations computed 不跨 midnight 重新分組 | low | done | 2026-04-24 /commit review | — |
| TD-023 | index.vue 雙 LazyChatConversationHistory 產生重複 fetch | low | done | 2026-04-24 /commit review | — |
| TD-024 | chat-history-sidebar test suite 品質(string contract/resolves) | low | done | 2026-04-24 /commit review | — |
| TD-025 | Container.vue $csrfFetch.native 跳過 CSRF header 造成 /api/chat 403 | high | done | 2026-04-24 code-quality-review-followups 人工檢查 10.x | — |
| TD-026 | index.vue 與 ConversationHistory fallback 重複 config + refresh 邏輯 | low | done | 2026-04-24 code-quality-review-followups /commit 0-A | — |
| TD-027 | MCP connector first-time authorization journey 實測待部署後驗證 | mid | open | 2026-04-24 auth-redirect-refactor 人工檢查 7.4 | — |
| TD-028 | DeleteAccountDialog Google reauth 無 callbackURL,dialog 會 unmount | mid | done | 2026-04-25 fix-delete-account-dialog-google-reauth 人工驗證 | — |
| TD-029 | mcp-toolkit alias fragility — shim 可能被 bypass | mid | done | 2026-04-24 fix-mcp-streamable-http-session review MI-2 | — |
| TD-030 | Claude.ai re-init 循環阻擋 tools/call(stateless 不足) | high | done | 2026-04-24 fix-mcp-streamable-http-session post-deploy | — |
| TD-040 | Token revoke 未同步清 MCP session DO | low | done | 2026-04-24 upgrade-mcp-to-durable-objects Task 4.6 | — |
| TD-041 | DO tool dispatch 未 wire up,flag=true non-initialize 回假 ack | high | done | 2026-04-24 upgrade-mcp-to-durable-objects Phase 4 trim | — |
| TD-042 | Local NuxtHub dev KV binding 未注入 cloudflare.env → /mcp 503 | mid | done | 2026-04-24 add-mcp-tool-selection-evals 5.2 apply | — |
| TD-043 | Evalite afterAll 的 process.exit / throw 不 propagate 到 pnpm eval | low | done | 2026-04-24 add-mcp-tool-selection-evals 6.5 verify | — |
| TD-044 | session.create.before 靜默吞 user_profiles UNIQUE 衝突 → better-auth user id 與 user_profiles.id 可能漂移 | mid | done | 2026-04-25 consolidate-conversation-history-config §7.4 人工檢查 | — |
| TD-045 | Local dev bootstrap 連串斷點(narrow scope:.env AI_SEARCH_INDEX 空值 + [nuxt-hub] DB binding not found 間歇 500;migration 自動化已由 NuxtHub v0.10.7 接手) | mid | in-progress | 2026-04-25 consolidate-conversation-history-config §7.4 人工檢查 | — |
| TD-046 | agentic-rag-staging AutoRAG index 在 CF 帳號中不存在(wrangler / Notion / deploy.yml 皆引用,CF API 僅有 agentic-rag) | high | done | 2026-04-25 consolidate-conversation-history-config §7.4 人工檢查 | — |
| TD-047 | /api/chat SSE ready 後階段 error 時 Container 未 emit conversation-persisted → DB 已建 conv 但 UI 不更新 | mid | done | 2026-04-25 consolidate-conversation-history-config §7.4 人工檢查 | — |
| TD-048 | 聊天 UI 缺顯式「新對話」入口 — sessionStorage 記住 active id 後只能靠刪除或 DevTools 清才能開新對話 | mid | done | 2026-04-25 consolidate-conversation-history-config §7.2 人工檢查 | — |
| TD-049 | Cloudflare Pages deploy API 拒絕 git HEAD commit message(Invalid commit message UTF-8 string [8000111]) | mid | done | 2026-04-25 v0.43.0 deploy run 24908303837 | — |
| TD-050 | Production / staging demo seed 已具備完整 D1 evidence、R2 metadata 與 AI Search active/current retrieval | mid | done | 2026-05-04 production / staging mock data audit | — |
| TD-051 | libsql legacy_alter_table=1 與 0007/0009 RENAME-rewrite 假設衝突 → account/session/passkey FK 在 fresh local DB 殘留 user_new ref,OAuth login 報 unable_to_link_account | high | done | 2026-04-25 dev server 報 unable_to_link_account | — |
| TD-052 | passkey-first-link-google.spec.ts 的 hubDb.transaction stub 沒覆蓋 syncUserProfile migrate path 的 tx.update().set().where() chain | low | done | 2026-04-25 wire-do §5.x SSE Tests cross-spec failure | — |
| TD-053 | fix-user-profile-id-drift production observation — 立即採樣 wrangler tail --env production 5-10 分鐘 + 撈最近 24h logs 搜 user_profiles sync failed 確認無預期外觸發 | low | done | 2026-04-25 fix-user-profile-id-drift archive | — |
| TD-054 | add-new-conversation-entry-points Safari private mode 實機驗證 — archive 時授權 skip,待後續本機 Safari 補上 | low | open | 2026-04-25 add-new-conversation-entry-points archive | — |
| TD-055 | TD-051 的漏網之魚:query_logs.mcp_token_id / messages.query_log_id / citation_records.query_log_id FK 在 fresh local DB 殘留 mcp_tokens_new / query_logs_new ref(migration 0010 的 RENAME-rewrite 假設失敗),任何 chat insert 直接炸 SQLITE_ERROR | high | done | 2026-04-26 add-sse-resilience 7.1 local heartbeat 驗證 | — |
| TD-056 | Workers AI judge 模型 max_completion_tokens: 200 上限被截斷 → JSON parse 失敗 → pipeline_error | low | open | 2026-04-26 v0.50.0 production 7.2 verify 抽查 query_logs | — |
| TD-057 | evlog wide event lifecycle 警告 — log.error() 在 wide event emit 後呼叫,導致 SSE stream 真實錯誤 keys 被丟棄 | mid | open | 2026-04-26 production wrangler tail | — |
| TD-058 | Production user_profiles 6 條 orphaned rows(profile.id 不在 user.id) | low | open | 2026-04-26 TD-053 production 立即驗收 | — |
| TD-059 | E2E Tests CI workflow 連續 50+ run 全紅 — nuxt preview webServer 啟動時 Node ESM loader 撞 cloudflare: protocol scheme,整個 webServer exit 1 | high | done | 2026-04-26 v0.50.1 E2E run 24940734040 | — |
| TD-060 | Production agentic-rag AutoRAG 對 seed acceptance fixture 的 retrieval_score 平均 0.32–0.44,全部低於 directAnswerMin=0.7,治理層 100% 走 no_citation_refuse | high | open | 2026-04-26 main-v0.0.54-acceptance run | — |
| TD-061 | Production query_logs r2 重測批次 28.6%(10/35)觸發 decision_path=pipeline_error;同 prompt 重複查詢可能觸發 stateful failure | high | open | 2026-04-26 main-v0.0.54-acceptance run | — |
| TD-062 | rag-query-rewriting 三個 entry point 的 retrieve closure 幾乎重複(chat.post.ts / mcp/tools/ask.ts / mcp/tools/search.ts),約 28 LoC × 3 應抽 helper | mid | open | 2026-04-26 /commit 0-A simplify review | — |
| TD-063 | useRewriter: false on retry 的 docstring 在 4 個 callback signature 重複 6-9 行同一段;應只留一份 canonical 在 knowledge-query-rewriter.ts | low | open | 2026-04-26 /commit 0-A simplify review | — |
| TD-064 | test/integration/retrieve-verified-evidence-with-rewriter.spec.ts 同時 mock search 與 resolveCurrentEvidence,違反「no mocking DB in integration tests」;應 relocate 或補真實 D1 round-trip 覆蓋 audit dynamic UPDATE | mid | open | 2026-04-26 /commit 0-A code-review | — |
| TD-065 | UpdateQueryLog.rewriterStatus 型別 string | null 與 query_logs.rewriter_status NOT NULL 不一致;潛在 5xx | low | open | 2026-04-26 /commit 0-A code-review | — |
| TD-066 | retrieveVerifiedEvidence 用 === 'success' 比對 RewriterStatus,違反專案 switch + assertNever exhaustiveness rule;新增 enum 值時不會 compiler error | low | open | 2026-04-26 /commit 0-A code-review | — |
| TD-067 | test/tsconfig.json baseline 191 errors(component module not found + fixture type drift + Nitro route key excessive depth + allowImportingTsExtensions 缺 + middleware signature 漂移) | mid | open | 2026-05-04 clade v0.3.10 cutover pre-push test-typecheck 揭露 | — |
| TD-070 | rag-query-rewriting 人工檢查對齊新 manual-review 規範(補 [discuss] marker + verify channel + Pre-Review Data Readiness) | mid | open | 2026-05-12 clade v1.3.6 manual-review.md 新規散播 | — |
| TD-071 | AutoRAG → AI Search API migration:production deployed v0.57.1(2026-06-09) | critical | done | 2026-06-09 rag-query-rewriting acceptance blocked | — |
TD-001 — mcp-token-store libsql 不相容
Status: done Resolved: 2026-04-20 — createMcpTokenStore() 3 個 function 遷移 Drizzle ORM(commit 1f6a4d1)+ 新增 test/integration/mcp-token-store.spec.ts 8 test cases 覆蓋 CRUD / scope / expiry / touch Priority: low Discovered: 2026-04-20 — member-and-permission-management 人工檢查 #10 Location: server/utils/mcp-token-store.ts (createMcpTokenStore() 的 createToken / findUsableTokenByHash / touchLastUsedAt — revoke 在 createMcpTokenAdminStore() 已是 Drizzle,不在本 TD 範圍) Related markers: search @followup[TD-001] in repo
Problem
mcp-token-store 使用 D1 $client.prepare() raw API。Local dev 用 libsql(see scripts/patch-hub-db-dev.mjs),$client 不支援 .prepare() / .bind() / .first() 等 D1-specific method,call 時拋 database.prepare is not a function。
影響:
- Local dev 無法 call
/mcpendpoint 的 Bearer token 驗證流程 - B16 人工檢查 #10(Guest MCP askKnowledge 錯誤碼)local 跑不起來,只能 production 驗
- 未來若要 local 端寫 integration test 覆蓋 MCP 流程也會撞牆
Production (Cloudflare Workers + D1) 運作正常。
Fix approach
改用 Drizzle ORM:getDrizzleDb() 取代 getD1Database()。createMcpTokenStore() 的 3 處 raw SQL 皆有對應 drizzle 表達式:
createToken→db.insert(schema.mcpTokens).values({...})findUsableTokenByHash→db.select(...).from(schema.mcpTokens).where(and(eq, eq, eq)).limit(1)+ JS 層 expires check 保留(避免跨 dialect NULL 比對語意差異)touchLastUsedAt→db.update(schema.mcpTokens).set({ lastUsedAt }).where(eq(...))
同檔 createMcpTokenAdminStore()(含 revokeTokenById / listTokensForAdmin / countTokensForAdmin)早已是 Drizzle,作為 canonical pattern 參考,不在本 TD 遷移範圍。其他 repo 內仍使用 getD1Database() 的 callers(server/api/* / server/mcp/tools/* / server/tasks/retention-cleanup.ts)可留作 future TD 評估。
Acceptance
- Local
pnpm dev可 call/mcp並通過 Bearer token 驗證(沒database.prepare is not a function) - 新 spec
test/integration/mcp-token-store.spec.ts覆蓋 CRUD + scope check - B16 人工檢查 #10 local 可重跑,GUEST_ASK_DISABLED / ACCOUNT_PENDING 皆通過
TD-002 — guest_policy DB-direct UPDATE 造成 cross-Worker cache drift
Status: done Resolved: 2026-04-20 — 選項 A 落地:新增 docs/runbooks/guest-policy.md runbook + setGuestPolicy() JSDoc 加反向說明 Priority: mid Discovered: 2026-04-20 — member-and-permission-management 人工檢查 #7(production 驗證) Location: server/utils/guest-policy.ts(cache 機制)+ operator documentation Related markers: search @followup[TD-002] in repo
Problem
setGuestPolicy() 的跨 Worker cache invalidation 依賴 KV guest_policy:version stamp:
setGuestPolicy寫 D1system_settings+ bump KV version- 每個 Worker instance 下次 request
getGuestPolicy()讀 KV version → 與 cached mismatch → 重讀 D1
B#7 驗證時發現:若 admin 繞過 API 直接 DB UPDATE(例如 wrangler d1 execute DB --remote --command "UPDATE system_settings..." 或 D1 console),不會 bump KV version,結果是:
- 已有 cached policy 的 Worker instance 繼續回舊 policy
- 沒 cache 的 cold instance 讀到新 policy
- 實測 5 個 parallel request 出現混合結果(2 個舊 / 3 個新)
影響:
- Operational risk:admin 手動改 DB 做緊急 rollback 時會遇到「改了 DB 但 Worker 還回舊值」的困惑
- 沒有告警機制
Fix approach
選項 A(文件化 + 弱提醒):
- 在
docs/runbooks/guest-policy.md寫明「必須透過 PATCH /api/admin/settings/guest-policy」 setGuestPolicy()的 JSDoc 加反向說明(繞過後果)
選項 B(程式層防線):
- 每次
getGuestPolicy讀 D1updated_attimestamp 與 KV version 比對;若 D1 timestamp 比 KV version 新 → 強制重讀 + 自動 bump KV - 成本:每次 request 多讀 D1 一欄(或整 row);與原設計「KV version 快路徑」trade-off
- 適用性:
guest_policy讀頻高(每次 chat + MCP 都 hit),原設計刻意避免 D1 讀;加這條會退化 p99
選項 C(隔離通道):
- 拿掉 DB-direct 權限(IAM 層);所有寫入必須過 API
- 最徹底但超過 code scope
建議:先做選項 A(低成本即見效),選項 B 視 operator 實際誤操作頻率決定。
Acceptance
- 選項 A:
docs/runbooks/guest-policy.md建檔並在 onboard 文件引用 - 選項 B(若採納):
guest-policy.spec.ts新增「D1 寫入繞過 API 時下次 request 會發現並自動補 bump」的 test
TD-003 — text-dimmed 對比度不足(cross-change residual)
Status: done Resolved: 2026-04-20 — 7 個檔共 13 處 text-dimmed → text-muted(commit 3a01a9f),regression guard e2e/td003-contrast.spec.ts 7/7 pass Priority: mid Discovered: 2026-04-20 — responsive-and-a11y-foundation C#11.9(axe-core 掃到三頁 color-contrast violation) Location:
app/pages/admin/debug/query-logs/[id].vue:180,189app/components/debug/OutcomeBreakdown.vue:59app/components/debug/EvidencePanel.vue:53,70,81app/components/debug/LatencySummaryCards.vue:49,59,69,79,88app/components/debug/ScorePanel.vue:34,49app/pages/auth/callback.vue:41app/components/documents/UploadWizard.vue:646,904- 其他未被 axe-core 初始 scan 覆蓋的頁面
Related markers: search @followup[TD-003] in repo
Problem
Nuxt UI text-dimmed token 在 dark theme 下 computed color oklch(0.556 0 0) on parent bg-default,對比度 ≈ 3.5:1(需 WCAG AA 4.5:1)。本次 session 已修 footer + chat UI 7 處(ConversationHistory / MessageList / RefusalMessage / index.vue / default.vue footer),但其他頁面約 10+ 處仍殘留。
影響:admin/debug/ 下的 logs、latency cards、auth callback、UploadWizard step state 皆有 a11y 違規;若未來掃這些頁面會報 fail。
Fix approach
批次替換 text-dimmed → text-muted(semantic 更合適,對比度充足)。需審視每處語意:
- Muted text(「無資料」「尚未載入」etc):直接
text-muted✓ - Disabled state(UploadWizard pending step):可能需
text-toned或保留視覺區分,要 inline review
或升級 token:在 app.config.ts 或 app/assets/css/main.css 擴充 --ui-color-text-dimmed token 為更深色(但會影響 intended design system hierarchy)。
Acceptance
- axe-core playwright 掃
/admin/debug/**、/admin/tokens、/admin/query-logs、/auth/callback、/admin/documents/upload全 0 color-contrast violation docs/design-review-findings.md#10 的 Cross-Change 備註更新為 resolved
TD-004 — 首頁 Google login button 高度 36px < WCAG 40px
Status: done Resolved: 2026-04-20 — app/pages/index.vue UButton 加 class="py-3"(commit b277b31) Priority: high Discovered: 2026-04-20 — responsive-and-a11y-foundation B17,e2e/viewport-baseline.spec.ts 測試既有 fail Location: app/pages/index.vue(signed-out 分支 Google login CTA) Related markers: search @followup[TD-004] in repo
Problem
viewport-baseline.spec.ts 測試 / primary CTA (Google login) 於 360×640 viewport 的 touch target size:
- 測試期望:height ≥ 40px(內註 WCAG 2.5.5 minimum 44×44)
- 實測:36px
- Fail 已存在一段時間,屬 B17 既有技術債
WCAG 2.5.5 Target Size (AA) 要求 touch target ≥ 44×44 CSS px。36px 對手指觸控、運動障礙使用者是明顯風險。
Fix approach
- 首頁 Google login button 改用較大
sizeprop 或加py-3讓高度 ≥ 44px - 同時掃首頁其他 interactive element 的 hit-target
- 跑
viewport-baseline.spec.ts驗證 pass
Acceptance
e2e/viewport-baseline.spec.ts全部 pass(包含「primary CTA ≥ 40px」)- Chrome DevTools 手動實測 iPhone SE viewport 下 button 能輕易點到
- 若連帶觸發其他頁面 hit-target audit 失敗,併入同一修
TD-005 — Admin 頁面 a11y violations 批次(@nuxt/a11y 首輪掃描)
Status: done Resolved: 2026-04-20 — UFormField 包裹 + srOnlyHeader utility + heading-order 全分支修復 + aria-labelledby 單一 source of truth(commit 285482b) Priority: high Discovered: 2026-04-21 — responsive-and-a11y-foundation 將社群版 nuxt-a11y 切換為官方 @nuxt/a11y@1.0.0-alpha.1,DevTools panel 首輪掃描結果 Location: /admin/query-logs、/admin/documents、/admin/tokens、/admin/debug/latencyRelated markers: search @followup[TD-005] in repo Related findings: docs/design-review-findings.md 2026-04-21 section #13-18
Problem
@nuxt/a11y DevTools panel 首次全站掃描發現 admin 頁面群有以下違規(非 MPM / RAF scope,屬 Cross-Change DRIFT):
Critical(5 elements, 阻擋 WCAG AA):
/admin/query-logs— 3 ×button-name:icon-only<UButton>缺aria-label(可能是 refresh / export / 詳情按鈕)/admin/query-logs— 2 ×label:form element 缺<label>綁定(可能是篩選器的 USelect / UInput)
Moderate(1 element):
/admin/debug/latency—heading-order:heading 層級跳階(例如 h1 → h3 漏 h2)
Minor(3 elements):
/admin/query-logs— UTableempty-table-header/admin/documents— UTableempty-table-header/admin/tokens— UTableempty-table-header
Scope 歸屬
/admin/query-logs→admin-query-log-uicapability/admin/documents→admin-document-management-uicapability/admin/tokens→admin-token-management-uicapability/admin/debug/latency→debug-decision-inspectioncapability
不併入 MPM / RAF archive(維持 scope discipline),獨立 low-friction PR 處理。
Fix approach
- button-name(icon-only button):給
<UButton icon="..." />加aria-label="..."prop - label(form element):
<USelect>/<UInput>若無 visible label,加aria-label- 或用
<UFormField label="...">wrap 提供語意 label
- heading-order:檢查
/admin/debug/latency的 heading 結構,補齊中間層級 - empty-table-header(UTable actions column):
- 統一 pattern:
header: () => h('span', { class: 'sr-only' }, '操作') - 參考
/admin/members的修復(2026-04-21 已落地)作為 canonical pattern - 建議抽成 utility:
srOnlyHeader(label: string)於shared/utils/table.ts
- 統一 pattern:
Acceptance
- @nuxt/a11y DevTools 對
/admin/query-logs、/admin/documents、/admin/tokens、/admin/debug/latencycritical + serious + moderate + minor 全數 0 violation - axe-core playwright 複掃驗證
docs/design-review-findings.md2026-04-21 section #13-18 標記 ✅ Resolved
TD-006 — Nuxt UI subtle variant tonal badge 對比度不足
Status: done Resolved: 2026-04-20 — 按 nuxt/ui #1284 官方推薦做 per-component compoundVariants override in app/app.config.ts(badge / alert / button × {primary, info, success, warning, error} × {subtle, soft} 共 30 entries,text 用 -700 dark:-200 shade 達 WCAG AA)+ 附帶修 2 處 raw text-{color} (app/pages/admin/debug/query-logs/[id].vue redaction notice、app/components/chat/MessageInput.vue validation/char-count)。e2e/td003-contrast.spec.ts 移除 4 個 detail page .exclude(...) 並 7/7 pass Priority: mid Discovered: 2026-04-20 — TD-003 e2e/td003-contrast.spec.ts 掃 /admin/debug/query-logs/[id] 時 axe-core 回報,用 .exclude() 暫時排除 Location:
app/pages/admin/debug/query-logs/[id].vue(redaction notice<p class="text-warning">、refusal badge、pii_request badge、score badge)- 任何使用
bg-{color}/10 + text-{color}subtle pattern 的 UBadge / UButton / 其他 Nuxt UI 元件
Related markers: search @followup[TD-006] in repo
Problem
Nuxt UI subtle variant 的 tonal 配色在 bg-default 上對比度不足 WCAG AA 4.5:1(小字)/ 3:1(大字 / 非文字):
| Selector | FG 色碼 | BG 色碼 | 實測對比度 | WCAG AA 要求 |
|---|---|---|---|---|
p.text-warning (redaction notice) | #f0b100 | #ffffff | 1.91:1 | 4.5:1 |
pii_request badge (bg-warning/10 + text-warning) | #f0b100 | #fef7e5 | 1.78:1 | 4.5:1 |
超出允許範圍 refusal badge (bg-error/10 + text-error) | #fb2c36 | #ffeaeb | 3.3:1 | 4.5:1 |
評審通過 score badge (bg-success/10 + text-success) | #00c950 | #e5faee | 2.03:1 | 4.5:1 |
這是 Nuxt UI design system token 層級的問題,不是單一 component 使用錯誤。subtle variant 以 10% opacity tint 作 bg,text 保留原 token 的中飽和度顏色 — warning / error / success 三色的中飽和度都低於 AA 門檻。
Fix approach
可能路徑(需討論):
- 調整 Nuxt UI theme token:
app.config.ts或main.css覆蓋--ui-color-warning / --ui-color-error / --ui-color-success為更深色版本(例:warning.600→warning.700)。影響範圍:所有用這些色 token 的 component。 - 換 variant:redaction notice / badge 改用
solidvariant(full-saturation bg + white text)或outlinevariant。代價:視覺層次變強,可能與 design 風格衝突。 - 升級 Nuxt UI:若上游已修,升版即可。查 changelog。
- 接受現狀 + 文件化 exception:在 design-review-findings.md 和 a11y report 明確宣告這些 token 組合為已知 exception;不掃 tonal badge。最差選項,但若修法成本過高可接受。
決策點:需與 design 討論哪個方向。不在當前 TD 範圍內自動選。
Acceptance
e2e/td003-contrast.spec.ts移除.exclude('p.text-warning')、.bg-warning\\/10、.bg-error\\/10、.bg-success\\/10四個排除- axe-core 對 detail page + 所有使用 tonal badge pattern 的頁面 color-contrast 0 violation
- 或:明確登記 design decision 為 exception,更新 findings 標記不再追蹤
TD-007 — 裝飾 icon tonal color 低於 WCAG 1.4.11 non-text contrast
Status: done Resolved: 2026-04-20 — Audit 全 repo 14 處 <UIcon text-{color}> 使用點,全部判定為 decorative(鄰近 heading / label / status text 已表達語義,icon 僅視覺重複)。12 處加 aria-hidden="true"(2 處原本已有)+ 0 處 informational,按 WCAG 1.4.11 decorative 圖形不計入 3:1 要求 Priority: low Discovered: 2026-04-20 — TD-006 code-review 掃同專案 raw text-{color} 時連帶發現 Location:
app/pages/account-pending.vue:36—<UIcon text-warning size-7>app/pages/admin/debug/latency/index.vue:131—<UIcon text-warning size-10>app/pages/admin/debug/query-logs/[id].vue:118,146—<UIcon text-warning/error size-10>app/pages/admin/documents/[id].vue:421—<UIcon text-primary size-5>app/components/chat/GuestAccessGate.vue:64—<UIcon text-warning>onbg-warning/10app/components/documents/LifecycleConfirmDialog.vue:103,105— 動態text-error/text-warningapp/components/documents/UploadWizard.vue:871,895,966—<UIcon text-error/primary>app/components/chat/CitationReplayModal.vue:121,138—<UIcon text-primary>
Related markers: search @followup[TD-007] in repo
Problem
WCAG 1.4.11 Non-Text Contrast (AA) 要求傳達資訊的 icon / 圖形元件對比度 ≥ 3:1。Nuxt UI 預設 text-{color} 指向 -500 shade:
text-warning(#f0b100) onbg-default(white) ≈ 1.9:1text-primary(#00c950) on white ≈ 1.8:1text-error(#fb2c36) on white ≈ 3.3:1 — 邊緣
全部低於 3:1(或僅邊緣 pass)。TD-006 scope 只含 text,不處理 icon 對比度;e2e/td003-contrast.spec.ts 目前只掃 color-contrast rule(針對 text),未納入 non-text-contrast rule。若未來擴充 axe 掃 wcag2aaa 或 non-text-contrast,此類會爆大量 violation。
Fix approach
與 TD-006 同策略不適用(compoundVariants 只覆蓋 component 的文字 class,不影響 icon 內的 SVG fill)。可能路徑:
- Icon 改用
text-{color}-700 dark:text-{color}-200:跟 TD-006 raw text 修法一樣,但 icon 視覺會變重 - 改用 solid chip 包住 icon:
<span class="rounded-full bg-warning p-2"><UIcon class="text-inverted" /></span>— full-sat bg + inverted(通常 white)text 對比 ≥ 4.5:1 - axe rule 維持不掃 non-text-contrast:若這些 icon 都是 decorative(
aria-hidden="true"),WCAG 1.4.11 不要求(只影響 informational icon)。先 audit 每個使用點的語意,decorative 的保留,informational 的才修
建議先做 audit(選項 3 的第一步),再依結果決定選項 1 / 2。
Acceptance
- 每處 icon 使用點明確標為 decorative(
aria-hidden="true")或 informational - Informational icon 對比度 ≥ 3:1(可實測驗證)
- 若擴充
e2e/td003-contrast.spec.ts加non-text-contrastrule,全 pass
TD-008 — acceptance-tc-0x MCP 整合測試在 TD-001 修後破損
Status: done Resolved: 2026-04-20 — commit 446c97d。Group B 11 個測試改用 createHubDbMock helper 一次補齊 getD1Database + getDrizzleDb 兩個 export;mcp-tool-runner 加 actor / tokenStore injection point + createStubMcpTokenStoreFromActor helper;刪除 TD-001 後失效的 stale raw SQL assertion。pnpm test:integration:16 failed → 0 failed(51/51 files / 260 passed + 1 skipped) Priority: mid Discovered: 2026-04-20 — 跑 pnpm test:integration 為了驗證 add-ai-gateway-usage-tracking 改動時發現 16 個 acceptance-tc-_.test.ts 失敗 Location: test/integration/acceptance-tc-_.test.ts(TC-01 / 04 / 06 / 07 / 08 / 09 / 10 / 11 / 12 / 13 / 14 / 16 / 17 / 18 / 19 / 20)+ test/integration/helpers/mcp-tool-runner.ts:68Related markers: 目前無 tasks.md marker;純 pre-existing broken test,non-blocking for add-ai-gateway-usage-tracking archive
Problem
TD-001 修復後(commit 1f6a4d1,mcp-token-store 遷移 Drizzle)兩類 failure 開始發生:
Error: [vitest] No "getDrizzleDb" export is defined on the "../../server/utils/database" mock— acceptance-tc-04 / 06 / 07 / 08 / 09 / 11 / 13 / 14 / 16 / 17 / 18 / 19 / 20。Drizzle 遷移後server/utils/database.ts新增getDrizzleDbexport,但這些 tests 的vi.mock('../../server/utils/database', ...)只 stub 原本的getD1Database,沒加getDrizzleDb。TypeError: Cannot read properties of undefined (reading 'id')atmcp-tool-runner.ts:68— acceptance-tc-01 / 10 / 12 的runMcpCase經由runMcpMiddleware解析 token,但 mock auth context 形狀對不上 Drizzle 遷移後的 shape(token record 的id欄位路徑改了)。
影響:
pnpm test:integration非綠,擋 CI(若有)與 commit 流程的「全綠再進 archive」慣例- Acceptance TC 無法 local 驗證 — 本次 add-ai-gateway-usage-tracking 的 AI binding 改動是否影響 MCP read path,只能靠 unit-level mock(
mcp-tool-ask.test.ts/mcp-tool-search.test.ts)推論;無 end-to-end 覆蓋
本次確認 pre-existing:git stash 我的 gateway 改動後跑 HEAD 上的 acceptance-tc-01.test.ts 仍 3/6 failed,failure log 完全一致。與 add-ai-gateway-usage-tracking 無關。
Fix approach
- 每個 acceptance-tc-*.test.ts 的
vi.mock('../../server/utils/database', ...)加getDrizzleDb: vi.fn().mockResolvedValue(...)stub,或抽共用 helpercreateDatabaseMock()(類似createHubDbMock)統一處理 mcp-tool-runner.ts的 token resolution 看當前 mcp middleware 的 token record shape,更新 mock auth context- 考慮把 D1/Drizzle 的 test mock centralize 到
test/integration/helpers/database.ts(目前createHubDbMock只處理 hub db,沒涵蓋getDrizzleDb單獨 export)
Acceptance
pnpm test:integration全綠(> 95% 通過,非 flake)acceptance-tc-01 / 04 / 06 / 07 / 08 / 09 / 10 / 11 / 12 / 13 / 14 / 16 / 17 / 18 / 19 / 20皆 pass- 未來 Drizzle schema 再變動時 mock 能跟上(helper 集中化的副產品)
TD-009 — user_profiles.email_normalized 全面改 nullable
Status: done Resolved: 2026-04-26 — passkey-user-profiles-nullable-email change archived 於 openspec/changes/archive/2026-04-26-passkey-user-profiles-nullable-email/。Migration 0016 8 表 cascade rebuild(user_profiles + 4 直接 FK children + 3 indirect chain children)+ schema.ts emailNormalized 改 nullable + auth.config.ts sentinel writer 改 null + syncUserProfile NULL branch 用 id-first lookup。實作中發現 _v16 → canonical FK pattern 在 D1 staging fail(commit bf7f423 revert),改用 _v16 → _v16 FK pattern + 依靠 D1 RENAME 自動 rewrite。已 ship v0.52.0 production deploy + smoke test 全綠。Evidence-based approval:integration spec 3/3 + unit 7/7 + wrangler D1 emulator + production smoke。 Priority: mid Discovered: 2026-04-21 — passkey-authentication change migration planning Location: server/db/schema.ts (userProfiles.emailNormalized), server/database/migrations/0009_passkey_and_display_name.sql (deferred from migration) Related markers: search @followup[TD-009] in repo
Problem
passkey-authentication change 的 design 原本規劃 user_profiles.email_normalized 同步改 nullable(落實 Decision 2 的完整語意),但 migration 0009 實務評估後延後:
user_profiles的 FK children 包含conversations、query_logs、messages、documents,改email_normalizednullable 必須 rebuilduser_profiles+ 其 FK 子樹(仿 0007 的 D1 cascade 模式)- 0009 本身已經 rebuild
user樹的 8 張表(user / account / session / member_role_changes / mcp_tokens / query_logs / citation_records / messages),再加user_profiles樹 rebuild 會讓 migration 超過安全 review surface(估計 700+ 行 SQL,大量 edge case) - 目前的 workaround:passkey-only 使用者的
email_normalized寫入 sentinel 值'__passkey__:' || user.id(保證 unique by PK),isAdminEmailAllowlisted不會誤判(sentinel 含:,不是合法 email 字元)
Fix approach
獨立 change passkey-user-profiles-nullable-email,單一職責:
- Migration 0010 rebuild
user_profiles+ FK children(conversations、query_logs、messages、documents) user_profiles.email_normalized改NULL+ partial unique index(WHERE email_normalized IS NOT NULL AND email_normalized NOT LIKE '__passkey__:%')- Data migration:掃 sentinel 值 → 改為 NULL
- 更新
server/utils/upsert 邏輯不再寫 sentinel - 更新
auth-storage-consistencyspec requirement(移除 sentinel scenario,換成純 nullable scenario)
Acceptance
PRAGMA table_info(user_profiles)顯示email_normalized允許 NULL- 原 passkey-only 使用者 row 的
email_normalized = NULL(sentinel 已遷移) - 相關查詢 code path(
isAdminEmailAllowlisted等)皆加email_normalized IS NOT NULLguard PRAGMA foreign_key_check零 rowspectra analyze對passkey-authenticationarchived spec 的 nullable rule 生效
TD-010 — credentials / admin-members endpoint libsql 不相容
Status: done Resolved: 2026-04-23 — portable ORM refactor、local happy-path 響應式驗證與 production /account/settings + /admin/members manual regression evidence 全數補齊,drizzle-refactor-credentials-admin-members closeout 條件滿足 Priority: mid Discovered: 2026-04-21 — passkey-authentication §16 Design Review 跑 /review-screenshot 時,/account/settings 與 /admin/members 兩頁回 500 Location:
server/api/auth/me/credentials.get.ts(db.all(sql\SELECT ... COALESCE(display_name, "displayName", name) ...`)`)server/api/admin/members/index.get.ts:127-164(db.all(sql\... EXISTS (SELECT 1 FROM account) ...`)`)
Related markers: search @followup[TD-010] in repo
Progress update (2026-04-21):
- Local dev
http://localhost:3010以/api/_dev/login建立admin@test.localsession 後重新驗證 TD-010 happy path。 /api/auth/me/credentials回 200:email = "admin@test.local",displayName = "Test Admin",hasGoogle = false,passkeys = []。/api/admin/members?page=1&pageSize=20回 200,data.length = 17,列資料包含displayName/credentialTypes/registeredAt/lastActivityAt。- Playwright 載入
/account/settings與/admin/members皆 HTTP 200、停留在目標 URL、未偵測已知 error text;截圖在screenshots/local/td010-continuation/。 - 2026-04-23 使用者於 production admin session 手動驗證:
/account/settingshappy path 正常顯示 email / display name / passkey / Google 綁定區塊;/admin/membershappy path 正常顯示會員列表 / role badge / credential badges / last activity,且本次資料量全部落於單頁,未出現500/暫時無法載入會員清單/ error state。 - local UI、production D1 回歸與 §16 responsive pipeline 已全數補齊,Status 更新為
done。
Problem
兩個 endpoint 使用 db.all(sql\...`)raw SQL + tagged template(drizzle 的 D1-specific API),在 production D1 正常運作,但在 local dev 的 libsql 環境下db.all` 不存在/行為不同,導致 endpoint 500。同類型問題見 TD-001(已修)。
影響範圍:
/account/settings頁面無法在 local 渲染 happy path(永遠 error state)/admin/members列表無法在 local 渲染 happy path(永遠 error state)- §16 Design Review 響應式截圖 6/12 只能拍到 error state;happy path 留待 §17 人工檢查(在 production/或修完 TD-010 的 local)驗證
admin-members-list.spec.ts與admin-members-passkey-columns.spec.ts這類 integration test 若依賴 local libsql 會 mock/skip,production 側才真正驗證
Fix approach
仿 TD-001 做法,把 raw SQL 改寫為 Drizzle ORM query:
credentials.get.ts:SELECT email, display_name, hasGoogle, passkeys[]拆成 3 條 drizzle query(user / account filter providerId='google' / passkey by userId)- 取消
COALESCE(display_name, "displayName", name)— FD-001 既已改以fieldName: 'display_name'對齊 schema,drizzleschema.user.displayName直接讀到 snake_case 值
admin/members/index.get.ts:EXISTS (SELECT 1 FROM account WHERE providerId = 'google' ...)→ drizzleleftJoin+groupBy或 subquery(drizzle-orm 支援sql\EXISTS(...)`` inline but 需保 libsql 相容寫法)credentialTypes聚合改以 application-layer 組裝(查 account 後 reduce)registeredAt/lastActivityAtdrizzle query 直接可得(user.createdAt、session.createdAtmax)
Acceptance
- Local dev 環境(hub:db sqlite)執行
curl /api/auth/me/credentialswith 有 session 的 cookie → 200 with correct payload - Local dev 執行
curl /api/admin/memberswith admin cookie → 200 with correct payload - 再次跑
/review-screenshot應可拍到 happy path 的響應式佈局 - production D1 側回歸:
admin-members-list.spec.ts+admin-members-passkey-columns.spec.ts全綠
TD-011 — migration 0009 FK cascade 設計不符 self-delete / audit 語意
Status: done Resolved: 2026-04-23 — migration 0010_fk_cascade_repair.sql 已套用至 production D1,並以 v0.28.12 完成 passkey-only self-delete production closeout Priority: high Discovered: 2026-04-21 — passkey-authentication §17.8 passkey-only 自刪實測,/api/auth/account/delete 回 500 Failed query,sqlite FK 阻擋 user row 刪除 Location: server/database/migrations/0009_passkey_and_display_name.sql
member_role_changes(line ~296-304):FOREIGN KEY (user_id) REFERENCES user_new(id)無 ON DELETE 子句 → 預設 NO ACTION → 阻擋 user row 刪除mcp_tokens(line ~183):created_by_user_id TEXT NOT NULL REFERENCES user_new(id)同樣無 ON DELETE → 阻擋刪除
Related markers: search @followup[TD-011] in repo
Progress update (2026-04-21):
server/database/migrations/0010_fk_cascade_repair.sql已套用到 production D1agentic-rag-db(database_id3036df7f-d54b-4d36-a33d-ecbb551fc278)。- Pre-apply backup 已下載:
backups/backup-pre-0010-20260421.sql。 - Production baseline / post-apply row count 一致:
member_role_changes=2,mcp_tokens=3,query_logs=72,citation_records=37,messages=81,"user"=2。 - Post-apply PRAGMA 驗證通過:
foreign_key_checkempty;member_role_changes無 FK;mcp_tokens.created_by_user_id為ON DELETE CASCADE;query_logs.mcp_token_id為ON DELETE SET NULL。 - Local WebAuthn 自刪驗證通過:Playwright virtual authenticator 建立 passkey-first user
td011-mo8ftwv1,插入 localmcp_tokensrow 後完成/account/settings刪除流程;POST /api/auth/account/delete回 200、導回/、member_role_changes.reason = 'self-deletion'tombstone 保留、該 user 的 token count 回 0;截圖screenshots/local/td011-self-delete-local.png。 - Local
.data/db/sqlite.dbcompatibility DB 也已修正 query_logs / citation_records / messages FK rebind,query_logs.mcp_token_id指向 canonicalmcp_tokens(id) ON DELETE SET NULL。 - 2026-04-23 production closeout 已完成:
v0.28.12重新實測 passkey-only test user 自刪,generate-authenticate-options/verify-authentication//api/auth/account/delete//api/auth/sign-out全部回200;最終 hard redirect 回/,/api/auth/get-session回null,首頁恢復登入文案。 - 同輪 production D1 驗證:
member_role_changeslatest rowreason = 'self-deletion';"user"/passkey/mcp_tokens對該 test user 的 count 全為0。 - TD-011 已完成收尾,Status 改為
done,保留條目供後續追溯。
Problem
task 7.2 設計意圖:
member_role_changes寫入reason = 'self-deletion'後保留為 tombstone
但實際 migration 0009 給 member_role_changes.user_id 加上 FK 且沒有 ON DELETE 子句,SQLite 預設 NO ACTION = RESTRICT → 當存在 audit row 時,delete user row 被 DB 層阻擋。tombstone 完全無法寫入。
mcp_tokens.created_by_user_id 類似問題,雖然語意該是「user 刪除 → token 也失效(cascade)」,但 migration 也沒寫 ON DELETE CASCADE。
影響:
- Passkey-only user 自刪(§17.8 人工檢查)在 production + local 皆 500
- Audit tombstone 機制完全無效(
passkey-authentication的合規承諾 broken) - Admin 用
/api/admin/members/:userId刪除使用者也會撞同一顆石頭
本 session 曾套用 local-only 修正(直接 rebuild 兩個表);後續已由 migration 0010 正規化並套用到 production D1。
Fix approach
新 migration 0010_fk_cascade_repair.sql(範圍於 2026-04-21 spectra-apply 兩次擴大:先擴成 5 表 rebuild 鏈,再加 query_logs 語意變更):
member_role_changes:rebuild 移除 FK constraint(audit tombstone 需要在 user 刪除後仍存活,所以user_id只是純 text reference,不設 FK)。indexidx_member_role_changes_user_created保留。mcp_tokens:rebuild 把created_by_user_id改為REFERENCES "user"(id) ON DELETE CASCADE,讓 token 隨 user 刪除自動清除。query_logs:rebuild 把mcp_token_id改為REFERENCES "mcp_tokens"(id) ON DELETE SET NULL(若保持預設NO ACTION則 user → mcp_tokens CASCADE 會被此 FK RESTRICT,TD-011 的 bug 只會往下移動一層;TDD red 測試已證實)。observability log 保留,token 歸屬在 token 已刪除後 NULL 化。citation_records/messages:連帶 FK re-bind 到新query_logs,columns 與 ON DELETE 子句完全保持 0009 狀態。- 走 0007 / 0008 / 0009 的 rebuild 模式(
PRAGMA defer_foreign_keys = ON→*_new+INSERT SELECT→ children-firstDROP→RENAME)。children-first DROP(messages → citation_records → query_logs → mcp_tokens)避免messages.query_log_id ON DELETE SET NULL在 DROP query_logs 時靜默觸發。 - Release checklist:在 production D1 apply 前確認備份 + 五張 rebuild 表 row count 對照。
Acceptance
PRAGMA foreign_key_check對member_role_changes/mcp_tokens/query_logs/citation_records/messages回 emptyPRAGMA foreign_key_list(query_logs)顯示mcp_token_id的on_delete = 'SET NULL'DELETE FROM "user" WHERE id = '<passkey-only-test-user>'成功(由/api/auth/account/delete觸發),audit row 保留,相關 token CASCADE 清除- user 刪除後其 query_logs 仍存在,
mcp_token_id為 NULL,query_redacted_text/created_at/channel/environment不變 - §17.8 人工檢查 local + production 皆通過
test/integration/passkey-self-delete.spec.ts新增 test case 覆蓋 audit tombstone 存在時能成功刪除 user,以及 query_logs 在 token cascade 後保留且mcp_token_id = NULL
TD-012 — passkey-first → link Google 被 better-auth email 檢驗擋住
Status: done Resolved: 2026-04-23 — 透過 passkey-first-link-google-custom-endpoint change 落地 custom endpoint pair,完成 local / production 人工驗證、allowlist 升權驗證與 archive Priority: high Discovered: 2026-04-21 — passkey-authentication §17.3 實機測試 passkey-first 帳號點 /account/settings 的「綁定 Google 帳號」,/api/auth/link-social 回 200 但 OAuth callback 回 please_restart_the_process,後端 log 顯示 Failed to parse state: link.email expected string, received null
Location:
app/pages/account/settings.vuehandleLinkGooglecall path- better-auth core
parseGenericState/link-socialendpoint(node_modules/better-auth/dist/api/routes/account.mjs約 line 148)要求session.user.email非空
Related markers: search @followup[TD-012] in repo
Problem
better-auth linkSocial endpoint 在建構 OAuth state 時,把 session.user.email 塞進 link.email 欄位並用 Zod 驗證必須是 string。passkey-first 帳號(email = NULL)直接通不過 state parse → OAuth callback 拒絕。
這是 better-auth 設計層的限制(intent 是用 email 比對防 account takeover),無法透過 allowDifferentEmails: true 之類 config 繞過;config 在 parse 之後才生效。
影響:passkey-authentication 的 Decision 5 / §17.3 scenario「passkey-first 使用者綁 Google」無法透過 better-auth 原生 API 實作。
Fix approach
新增 custom endpoint pair(繞開 better-auth linkSocial,自建 OAuth flow):
GET /api/auth/account/link-google-for-passkey-firstrequireUserSession+ 驗session.user.email === null- 建 OAuth state(自己的 cookie / KV key,帶 session.user.id)
- redirect 到 Google authorization URL(用現有
NUXT_OAUTH_GOOGLE_CLIENT_ID與 redirect_uri)
GET /api/auth/account/link-google-for-passkey-first/callback- 收 Google
code+ 自家 state - 用 code 換 access token + id_token(直接 fetch Google token endpoint)
- 解 id_token 取 email / name / image
- 檢查 email 是否已在其他 user.id 上使用 → 若 yes 回
EMAIL_ALREADY_LINKED409 UPDATE "user" SET email = <google-email>, image = <google-image> WHERE id = <session.user.id>INSERT INTO account (userId, providerId='google', accountId, accessToken, idToken, refreshToken, scope, createdAt, updatedAt) VALUES (...)跟 better-auth schema 對齊databaseHooks.session.create.before下次會自動走 reconciliation(跑 allowlist 比對 → 升 admin 若符合)- redirect 回
/account/settings?linked=google
- 收 Google
app/pages/account/settings.vue的handleLinkGoogle改指向新 endpoint(僅當credentials.email === null;否則仍用 better-auth linkSocial)
Acceptance
- passkey-first 使用者(email=NULL)點「綁定 Google 帳號」→ OAuth 走通 → email 填入 + Google account row 建立 + passkey row 保留 → 下次登入可用 passkey 或 Google 任一
- 衝突處理:若 Google email 已屬另一 user.id → 409 UX 顯示清楚
- Allowlist reconciliation:若綁的 Google email 在
ADMIN_EMAIL_ALLOWLIST→ 下次 session refresh 自動升 admin(既有session.create.before機制) test/integration/passkey-first-link-google.spec.ts覆蓋 happy path + 衝突 409 + allowlist upgrade
TD-013 — /account/settings 新增 passkey 缺 naming dialog
Status: done Resolved: 2026-04-21 — app/pages/account/settings.vue 新增 nameDialogOpen / passkeyNameInput state + UModal(輸入 passkey 名稱 + 驗證 + 傳給 client.passkey.addPasskey({ name })) Priority: low Discovered: 2026-04-21 — passkey-authentication §17.2 實機驗證 Google-first 加綁 passkey,列表顯示「未命名 passkey」 Location: app/pages/account/settings.vue handleAddPasskeyRelated markers: search @followup[TD-013] in repo
Problem
handleAddPasskey 直接呼叫 client.passkey.addPasskey() 沒有傳 name,passkey.name 欄位留空 → 列表顯示「未命名 passkey」,多個裝置時難以辨識(尤其 revoke 誤刪風險)。
Fix applied
加一個 naming dialog:點「新增 Passkey」不直接啟動 ceremony,先開 modal 讓使用者輸入名稱(maxlength 40,必填),確認後帶 name 呼叫 addPasskey。驗證失敗或空字串在 modal 內直接顯示 inline error。
TD-014 — error-sanitizer 後 12 test 拋 evlog Logger not init
Status: done Resolved: 2026-04-24 — 本地重跑 pnpm test:integration 已恢復全綠(72 files / 364 tests passed / 1 skipped),不再重現 evlog Logger not initializedPriority: mid Discovered: 2026-04-21 — drizzle-refactor-credentials-admin-members apply 階段跑 pnpm test:integration 發現,pre-existing 非本次引入 Location: 影響的 test 檔:
test/integration/acceptance-tc-ui-state.test.ts(5 個 sub-test)test/integration/admin-documents-route.test.ts(3 個)test/integration/dev-login-route.test.ts(2 個)test/integration/publish-route.test.ts(2 個)
Related markers: search @followup[TD-014] in repo
Problem
v0.25.0 commit df49b11 引入「全站 server API 錯誤訊息洩漏防護」的 error-sanitizer nitro plugin。該 plugin 改動了 evlog logger 的初始化時序,造成上述 12 個 integration test 在呼叫 handler 時拋出:
[evlog] Logger not initialized. Make sure the evlog Nitro plugin is registered. If using Nuxt, add "evlog" to your modules.影響:
pnpm test:integrationfull run 現在有 12 個紅燈(但全是 pre-existing / 非 TD-010 引入)- CI 如果有 test gate 會被擋;production D1 端 runtime 看起來 OK(handler 仍正常執行,只是 test stub 少了 plugin)
本 TD 不在 TD-010 scope(TD-010 是 refactor drizzle query builder,與 evlog plugin 時序無關),但 apply 階段發現需登記。
Resolution note
2026-04-24 重新執行 pnpm test:integration,受影響 integration suite 已不再出現 logger 初始化錯誤;目前 repo 內也沒有殘留 @followup[TD-014] marker。此條保留於 tech debt register 僅供追溯,不再列為 open item。
Acceptance
pnpm test:integration全綠(除非後續有其他新的 real regression)- 不得以
.skip繞過 - TD-014 marker 在修復 PR 的 tasks 標註 @followup[TD-014]
TD-015 — SSE 長連線缺 heartbeat,30s proxy timeout 風險
Status: done Resolved: 2026-04-26 — v0.49.0 add-sse-resilience 已 archive;v0.50.1 production 立即採樣(取代原 7 天觀察期):過去 7 天 production 共 75 條 query_logs(v0.49.0 deploy 後 v0.49.0+ 樣本:含人工觸發共 6 條 chat — 1 條 no_citation + 1 條 pipeline_error + 1 條 accepted 成功 + 1 條 no_citation markdown + 2 條 pipeline_error)。first_token_latency_ms 兩條健康樣本 = 7254 ms / 3245 ms(量測有效、非 0、非異常)。refusal_reason 分布皆屬業務正常 refusal 或 TD-056 已知 case;live wrangler tail 採樣 5 分鐘無 connection closed / NetworkError / 30s+ 阻塞訊號,acceptance 條件「chat.error 計數無顯著上升」滿足。Manual production URL 30s+ reasoning 驗收與長期 latency 抽查留作 backlog(不阻擋 done)。Live tail 順帶證實 TD-057 warning 在 production 真實重現(log.error × 3 + log.set × 3,全在 SSE 路徑),詳見 TD-057 entry。 Priority: mid Discovered: 2026-04-24 — /commit code-review(web-chat SSE streaming) Location: server/api/chat.post.ts:createSseChatResponseRelated markers: search @followup[TD-015] in repo
Problem
Cloudflare Workers SSE 經過 CF edge / 某些瀏覽器代理時,若長時間(~30s)沒資料會被主動關閉。createSseChatResponse 在發出 ready 後、收到 Workers AI 首 token delta 前,若生成延遲 > 30s,client 會看不到任何後續事件直接掉線。
Fix approach
在 ReadableStream.start 內啟動一條 keep-alive 迴圈,每 15-20 秒 enqueue 一個 SSE 註解行(: keep-alive\n\n),直到正常終止或 abort。需注意:
- 使用
AbortController讓 cancel 時能一起收尾 - 迴圈 enqueue 需檢查
closed旗標,避免 race - 時間間隔須顯著小於 CF 的 idle threshold
Acceptance
- [x] Chat SSE 連線在 Workers AI 首 token 延遲 ≥ 30s 時 client 仍持續收到事件流,不掉線(code 已 land —
add-sse-resiliencechange,archive 後 production deploy 驗) - [x] 新增 unit test 模擬 slow first token,assert heartbeat block 有被送出(
test/unit/chat-route-heartbeat.spec.ts,3 cases:emit / terminate stop / consumer cancel) - [ ] Manual QA 在 production 環境觀察 30s+ 延遲的 chat 不再觸發
NetworkError / connection closed(留作 backlog,不阻擋 done;建議:本機開 production URL 發一條會觸發長 reasoning 的 query 即時驗證) - [x] 立即採樣 production logs(取代原「7 天觀察」):2026-04-26 跑
wrangler d1 execute agentic-rag-db --remote撈 7 天 query_logs 統計 —accepted: 71 / blocked: 4,無 SSE heartbeat 引入的新 error 模式;refusal 分布皆屬業務正常或已知 TD-056。acceptance 條件滿足 - [x] 立即抽 production D1 最近樣本 first_token_latency_ms:v0.49.0 deploy 後唯一成功樣本 7254 ms(量測有效、非 0、非異常),確認 first-token 量測未被
: keep-alive行誤計(schema 改用first_token_latency_ms直接欄位、不需手動算 delta)
TD-016 — isAbortError / createAbortError 在四處重複實作
Status: done Resolved: 2026-04-25 — 抽到 shared/utils/abort.ts(export isAbortError + createAbortError,跨 browser / Workers runtime 適用),4 個 caller (app/utils/chat-stream.ts / server/api/chat.post.ts / server/utils/web-chat.ts / server/utils/workers-ai.ts) 改 import { ... } from '#shared/utils/abort' 並刪 local 實作。新增 test/unit/abort.spec.ts 12 cases 覆蓋 DOMException + duck typing + null/undefined/string/number negative + round-trip。 Priority: low Discovered: 2026-04-24 — /commit simplify review Location:
app/utils/chat-stream.tsserver/api/chat.post.tsserver/utils/workers-ai.tsserver/utils/web-chat.ts
Related markers: search @followup[TD-016] in repo
Problem
同一對 helper 在四個檔案各自實作,行為一致(檢查 DOMException.name === 'AbortError' / 建立 new DOMException('aborted', 'AbortError'))。若未來改語意(例如加 reason)需要四處同步。
Fix approach
抽共用到 shared/utils/abort.ts,四處 import。注意 shared 層需同時可用於 app(browser)與 server(Workers runtime),DOMException 在兩邊皆有。
Acceptance
- 四處改為
import { isAbortError, createAbortError } from '#shared/utils/abort' - 原本的 local function 刪除
- 既有 unit / integration test 持續綠
TD-017 — chat.post.ts 兩個 AI binding getter 可合併
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit simplify review Location: server/api/chat.post.ts:getRequiredAiSearchBinding / getRequiredWorkersAiBindingRelated markers: search @followup[TD-017] in repo
Problem
兩個 getter 都讀 getCloudflareEnv(event).AI、檢查某個 method 存在、拋 503。重複 skeleton。
Fix approach
抽共用:
ts
function requireAiBinding<T>(event: H3Event, input: { method: keyof T; message: string }): T {
const binding = getCloudflareEnv(event).AI
if (
!binding ||
typeof (binding as Record<string, unknown>)[input.method as string] !== 'function'
) {
throw createError({
statusCode: 503,
statusMessage: 'Service Unavailable',
message: input.message,
})
}
return binding as T
}或兩個 getter 共享一次 getCloudflareEnv(event).AI 讀取。
Acceptance
getRequiredAiSearchBinding/getRequiredWorkersAiBinding改為薄 wrapper- chat.post.ts 無行為變更,
pnpm test:integration綠
TD-018 — Container.vue classifyError 巢狀條件抽 lookup table
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit simplify review Location: app/components/chat/Container.vue:classifyErrorRelated markers: search @followup[TD-018] in repo
Problem
HTTP status code → error kind 的 mapping 目前用三元鏈 + 巢狀 if,第 3 層接近警戒線。
Fix approach
抽 readErrorStatus(error): number | undefined + STATUS_TO_KIND: Record<number, ErrorKind>,主邏輯 flatten。
Acceptance
classifyError扁平,單層邏輯- 既有
chat-container.spec.ts持續綠
TD-019 — SSE reader pattern 在 client/server 雷同可抽共用
Status: done Resolved: 2026-04-26 — add-sse-resilience change archive。新增 shared/utils/sse-parser.ts(export readSseStream + ReadSseStreamInput + SseBlock),統一處理 reader / decoder / buffer.split('\n\n') 主迴圈、abort race handling(signal.addEventListener('abort', () => reader.cancel(createAbortError())) 配合 main loop signal.aborted 檢查 + finally 清 listener + releaseLock)、comment block detection(: keep-alive 等註解 block 不轉發給 onBlock)。app/utils/chat-stream.ts:readChatStream 與 server/utils/workers-ai.ts:readStreamedTextResponse 改用 readSseStream,block handler 各自處理 chat event type / [DONE] sentinel + JSON delta,外部簽章與 emit 行為不變。新增 test/unit/sse-parser.spec.ts 10 cases 覆蓋 normal / multi-block / partial trailing / abort mid-stream / signal pre-aborted / UTF-8 邊界 / comment block / terminate / missing body;既有 chat-stream / workers-ai / chat-route integration spec 全綠。Land 在 v0.48.0 commit cd43bf2,archive 於 v0.49.0 後。 Priority: low Discovered: 2026-04-24 — /commit code-review Location: app/utils/chat-stream.ts:readChatStream + server/utils/workers-ai.ts:readStreamedTextResponseRelated markers: search @followup[TD-019] in repo
Problem
兩處都:reader.read() → decoder.decode → split('\n\n') → buffer.pop() → parseSseBlock,abort handler / finally / releaseLock 幾乎 1:1 雷同。目前分別維護,有漂移風險(例如一邊修 bug 另一邊漏改)。
Fix approach
抽 shared/utils/sse-parser.ts:
- 公共
readSseStream(response, { onBlock, signal })— 處理 reader / decoder / block 切分 / abort - 兩邊 caller 只需提供 block handler
注意:server 端的 block handler 需認識 [DONE];client 端需認識 event-type 解析。可透過 callback 傳入。
Acceptance
- [x]
app/utils/chat-stream.ts與server/utils/workers-ai.ts共用同一個 SSE reader - [x] 既有 unit / integration test 持續綠
- [x] 新增或擴充 sse-parser 的 unit test
TD-020 — CHATGPT_CONNECTOR_OAUTH_PATH_PATTERN 可收緊字元集
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review Location: server/utils/mcp-chatgpt-registration.ts:CHATGPT_CONNECTOR_OAUTH_PATH_PATTERNRelated markers: search @followup[TD-020] in repo
Problem
現行 regex /^\/connector\/oauth\/[^/?#]+$/ 允許任意字元(Unicode、. 等)。雖然 origin 已限定 https://chatgpt.com,但 segment 字元集可以更嚴。
Fix approach
改成 /^\/connector\/oauth\/[A-Za-z0-9_-]{1,64}$/,並確認 OpenAI Connector OAuth ID 實際允許的字元集(避免誤擋合法 segment)。
Acceptance
isAllowedChatGptConnectorRedirectUri拒絕含./ Unicode / 超長 segment 的 URI- 既有 unit test 仍綠;新增 case 涵蓋新限制
TD-021 — ConversationHistory bucket toggle 缺 aria-expanded;onExpandRequest 應轉 emit
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(collapsible-chat-history-sidebar archive) Location: app/components/chat/ConversationHistory.vue(bucket toggle button、onExpandRequest prop) Related markers: search @followup[TD-021] in repo
Problem
- bucket toggle
<button>沒有aria-expanded/aria-controls;目前靠 Nuxt UIUCollapsible的:open外控。e2e axe 已過,但 toggle 本身未對 AT 明示狀態變化 onExpandRequest?: () => void以 callback-prop 形式宣告,但同檔已用defineEmits管理conversation-cleared/conversation-selected,event 契約分裂在 props + emits 兩處
Fix approach
- 在 toggle 按鈕補
:aria-expanded="bucketOpenState[group.bucket]" - 將
onExpandRequest改為'expand-request': []emit,父層改綁@expand-request="expandHistorySidebar"
Acceptance
- 新增 axe / a11y 單元測試驗證 bucket toggle 的
aria-expanded依狀態更新 defineEmits宣告包含expand-request,index.vue改用@expand-request,e2e 仍綠
TD-022 — groupedConversations 不跨 midnight 重新分組
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review Location: app/components/chat/ConversationHistory.vue:groupedConversations computed Related markers: search @followup[TD-022] in repo
Problem
groupedConversations computed 只在 conversations 變動時 re-run,new Date() 在掛載當下被捕捉。若頁面長開跨過午夜,原本分到「今天」的對話不會自動移到「昨天」,需要 refetch 才會更新。
Fix approach
引入一個「當前時間」tick(如 useNow({ interval: 60_000 }) 或跨午夜的 one-shot timer),讓分組在日期切換時重新計算;或在 visibility change / refetch 觸發時強制重新分桶。
Acceptance
- 跨午夜後無需 refetch,時間桶自動重分類(有單元測試以假時鐘覆蓋)
- 不引入每秒重 render
TD-023 — index.vue 雙 LazyChatConversationHistory 產生重複 /api/conversations fetch
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(既存 pattern) Location: app/pages/index.vue(inline lg sidebar + drawer 分支各自掛 LazyChatConversationHistory) Related markers: search @followup[TD-023] in repo
Problem
<lg 的 drawer 與 lg 的 inline sidebar 各自 mount 一個 LazyChatConversationHistory;useChatConversationHistory 在每個 instance 以 immediate: true 觸發 /api/conversations,造成登入首次渲染時出現兩次並行 fetch。
Fix approach
把 useChatConversationHistory hoist 到 index.vue,將 state 以 props 傳給兩個 surface;或讓 drawer 以 v-if="historyDrawer.isOpen.value" 延後掛載,避免同時 mount。
Acceptance
- 首頁首次渲染只觸發一次
/api/conversationsGET(Network 驗證 + e2e assert) - 兩個 surface 仍顯示同一來源資料、互不衝突
TD-024 — chat-history-sidebar 測試品質:string contract + Playwright resolves
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(testing anti-patterns) Location:
test/unit/chat-history-sidebar-source-contract.test.tse2e/collapsible-chat-history-sidebar.spec.ts(約 L216-218)
Related markers: search @followup[TD-024] in repo
Problem
chat-history-sidebar-source-contract.test.ts全篇以readFileSync+toContain比對.vueraw source(class 片段、icon 名、aria-label 文字),任何無害重構(如 class 重排、把 icon 抽常數)會被誤判為違規,違反testing-anti-patterns.md的「test behavior, not source strings」- e2e spec 使用
await expect(page.evaluate(...)).resolves.toBe('true');@playwright/test的expect沒有.resolvesmatcher,可能 silently pass 而未真的 assert
Fix approach
- 把 source-contract test 轉為
mountSuspended元件測試(驗 DOM / aria / storage key 行為),或直接移除並信賴 e2e 覆蓋 - e2e spec 改為
expect(await page.evaluate(...)).toBe('true')形式
Acceptance
- 移除 / 改寫後
pnpm test+pnpm test:e2e仍綠 - 重構
.vue不再需要改 contract test - Playwright expect 對 evaluate 結果真正 assert(人工故意打壞邏輯可看到 red)
TD-025 — Container.vue $csrfFetch.native 跳過 CSRF header 造成 /api/chat 403
Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: high Discovered: 2026-04-24 — code-quality-review-followups 人工檢查 10.x 送不出 chat 訊息 Location: app/components/chat/Container.vue:193Related markers: 無(直接在 scope 內修)
Problem
2026-04-24 凌晨的 SSE streaming refactor(commit c6ea971)把 $csrfFetch('/api/chat', ...) 改成 $csrfFetch.native(...)。.native 是 ofetch 繼承自 globalThis.fetch 的 raw fetch,不經過 nuxt-csurf 的 onRequest hook,所以 x-csrf-token header 根本沒被加,server csurf 驗證固定 403 CSRF Token Mismatch。任何登入使用者送出 chat 訊息就會打到這個錯。
單元 / 整合測試都 mock 掉 fetch 或直接呼叫 server handler,不會 trigger .native 的真實呼叫路徑。所以 test 全綠、production 炸。
Fix approach
保留 .native(streaming 需要 raw Response + readable body)但手動用 useCsrf() 取 token 塞進 headers:
ts
const { csrf, headerName } = useCsrf()
const headers: Record<string, string> = {
accept: 'text/event-stream',
'content-type': 'application/json',
}
if (csrf && headerName) headers[headerName] = csrf
const response = await $csrfFetch.native('/api/chat', { method: 'POST', body, headers, signal })Acceptance
- 本地登入後送訊息能收到 streaming token / terminal event
- Network tab 看到 POST
/api/chatrequest headers 有對應 csrf header - 既有單元 / 整合 test 仍綠
TD-026 — index.vue 與 ConversationHistory fallback 重複 config + refresh 邏輯
Status: done Resolved: 2026-04-25 — consolidate-conversation-history-config change 抽出 createChatConversationHistory factory 集中管理兩處 config 與 refresh reconcile 順序。 Priority: low Discovered: 2026-04-24 — code-quality-review-followups /commit 0-A simplify review Location: app/pages/index.vue (~L44-60) + app/components/chat/ConversationHistory.vue (~L40-60) Related markers: search @followup[TD-026] in repo(archived change code-quality-review-followups 保留歷史 marker)
Resolution
app/composables/create-chat-conversation-history.ts 封裝 useChatConversationHistory config 組裝與 refreshAndReconcile(selectedId) 實作;app/pages/index.vue 與 app/components/chat/ConversationHistory.vue 兩處分別以 parent-provide 與 owner-fallback 方式呼叫同一 factory,toast / callback 文案集中維護。新增 test/unit/create-chat-conversation-history.spec.ts 覆蓋 refresh reconcile 四條路徑(still present / missing but loadable / missing + detail missing / no active id)+ 預設 toast fallback 兩條測試;spec web-chat-ui 新增「Conversation History Refresh Reconciliation」requirement 將 reconcile 順序正式化。
Problem
TD-023 引入 provide/inject 後,ConversationHistory.vue 保留 owner-fallback 分支(用於 test 或無 parent provide 的場景)。這條 fallback 分支把 parent index.vue 裡的 useChatConversationHistory({...}) config(listConversations / deleteConversation / loadConversation / 4 個 toast handler 回呼)與 refreshConversationHistory() 的 body(refresh → 查存在性 → detail fallback → cleared notification)幾乎逐行複製兩份。
- parent 和 owner-fallback 會在未來同步維護時漂移(如 toast 文案換、callback signature 改)
- Test 目前都透過
vi.mock攔截useChatConversationHistory,所以兩處的差異不會被 test 抓到 - 語義上 parent 永遠會 provide,owner-fallback 只為 test convenience,實際無使用者路徑會真的走 fallback
Fix approach
抽成 createChatConversationHistory($csrfFetch, toast, { onConversationSelected, onConversationCleared, selectedConversationId }) factory(放 app/composables/useChatConversationHistory.ts 或相鄰 utils):
- 回傳
{ api, refreshAndReconcile(selectedId?) } - parent
index.vue與 ConversationHistory owner-fallback 都呼叫同一個 factory - Toast / callback config 集中維護
或更激進:移除 owner-fallback 路徑,test 改用 createTestingPinia 之類 provide 真 instance 到 mount root,讓 production 與 test 走同一條路。
Acceptance
index.vue和ConversationHistory.vue沒有重複的useChatConversationHistoryconfig literal- 既有單元測試(
conversation-history-{aria,midnight,component}.spec.ts)仍綠 - e2e
chat-home-fetch-dedup.spec.ts仍綠 - Factory 有至少一個直接的 unit test 覆蓋 refresh reconcile 行為
TD-027 — MCP connector first-time authorization journey 實測待部署後驗證
Status: in-progress — local backend migration verified; staging deploy/smoke/D1/MCP evidence pending authorization. Priority: mid Discovered: 2026-04-24 — auth-redirect-refactor 人工檢查 7.4 Location: app/pages/auth/mcp/authorize.vue、app/utils/mcp-connector-return-to.ts、app/pages/auth/callback.vueRelated markers: search @followup[TD-027] in repo
Problem
auth-redirect-refactor 改動:
/auth/mcp/authorize的 Google login handler 加callbackURL: '/auth/callback'(避免 better-auth 預設回/)/auth/callbackconsume order 改為 MCP > generic > fallback/
以上改動需要透過 Claude.ai 實際發起 MCP connector connection 才能 end-to-end 驗證,但目前 local dev 無法被 claude.ai 直接連到(需 ngrok / cloudflare tunnel / 部署到 staging)。人工檢查 7.4 因此暫未驗證。
Fix approach
部署到 staging 或 production 後,執行人工驗收流程:
- Claude.ai MCP connector 指向已部署的 MCP endpoint
- 發起連接 → 被導去
https://<deployed-host>/auth/mcp/authorize?client_id=...&redirect_uri=...&... - 點 Google 登入 → OAuth 完成
- 必須回到原
/auth/mcp/authorize?...同樣 URL(驗saveMcpConnectorReturnTosessionStorage bridge) - 看到授權同意畫面 → 點授權 → 回 Claude.ai
- 在 Claude.ai 能正常使用 MCP tools
Acceptance
- Staging / production 完成上述 6 步流程無中斷、無錯誤
- 步驟 4 的 URL 是原始 authorize URL 含 query,而非
///auth/login - Claude.ai 端 connector 狀態顯示 connected 且可呼叫工具
- 完成後將 7.4 marker 從 tasks.md 移除並更新 TD-027 Status 為
done
TD-028 — DeleteAccountDialog Google reauth 無 callbackURL,dialog 會 unmount
Status: done Resolved: 2026-04-25 — fix-delete-account-dialog-google-reauth 已 archive;使用者完成 Google reauth delete flow 人工驗證,3010 本機補充驗證也覆蓋 direct ?open-delete=1 不繞過、dialog reauth gate、pending-delete resume confirm step。 Priority: mid Discovered: 2026-04-24 — auth-redirect-refactor code-review OBS-1 Location: app/components/auth/DeleteAccountDialog.vue handleGoogleReauthRelated markers: 尚無 tasks.md marker(pre-existing,非本 change scope)
Problem
handleGoogleReauth 呼叫 signIn.social({ provider: 'google' }) 未指定 callbackURL,better-auth 預設回 /。使用者流程:
/account/settings→ 按「刪除帳號」→ dialog 開啟 → 按「Google 重新驗證」- 跳 Google OAuth → 回到本站
/ - Dialog 已 unmount,
reauthComplete = true設在 unmounted instance - 使用者看到
/首頁,沒有任何指示「session 已 rotate」 - 必須從頭再開一次 delete 流程
Passkey reauth 是同 origin 不受影響。
Fix approach
兩個方向(擇一):
A. 加 callbackURL: '/auth/callback' + 透過 saveGenericReturnTo('/account/settings?open-delete=1') 指示 settings 頁自動重開 dialog 並跳到 confirm step。
B. 把 Google reauth 搬去獨立頁面 /account/settings/reauth(避免 dialog 依賴 mounted state)。
A 較簡單,沿用既有 saveGenericReturnTo bridge;B 架構更乾淨但範圍大。
Acceptance
- 刪帳號走 Google reauth 路徑時不會中斷
- 回到
/account/settings後 dialog 自動重開且已通過 reauth 檢查 - Passkey reauth 路徑無行為變化
TD-029 — mcp-toolkit alias fragility,shim 可能被 bypass
Status: done Resolved: 2026-04-25 — 方案 A 落地:新增 test/integration/mcp-production-wiring.spec.ts(8 tests),分兩組驗證:(1) 靜態檢查 nuxt.config.ts 的 agents/mcp → shim、mcpToolkitCloudflareProvider → mcpToolkitNodeProvider alias 規則 + shim 路徑 + toolkit cloudflare provider 仍用 agents/mcp specifier;(2) shim contract 行為驗證:POST initialize 回 Content-Type: application/json + valid JSON-RPC envelope(含 protocolVersion/capabilities/serverInfo),GET/DELETE 回 405 + Allow: POST。任一 alias drift 或 toolkit upstream 改 specifier 時 spec 立刻 fail。方案 B(patch upstream)留作長期計畫。 Priority: mid Discovered: 2026-04-24 — fix-mcp-streamable-http-session code-review MI-2 Location: nuxt.config.ts:312 alias mcpToolkitCloudflareProvider → mcpToolkitNodeProvider、server/utils/mcp-agents-compat.ts(shim) Related markers: 尚無 tasks.md marker(下一個該 change archive 前應登記到對應 tasks)
Problem
fix-mcp-streamable-http-session 的 fix(拒 GET/DELETE + 強制 JSON response)實作在 agents/mcp 的 shim(server/utils/mcp-agents-compat.ts)內。Shim 能生效依賴:
@nuxtjs/mcp-toolkit的 cloudflare provider(dist/runtime/server/mcp/providers/cloudflare.js)在執行時await import("agents/mcp")載入 shimnuxt.config.ts的 alias 將agents/mcp指到 shim 本身
風險:若 toolkit 將 provider 的載入方式改為其他 specifier(例如加 .js extension),或 nuxt.config.ts:312 的 mcpToolkitCloudflareProvider → mcpToolkitNodeProvider alias 規則在某次 bundle 條件下命中,則 cloudflare provider 會被替換成 node provider,shim 不被載入:
- node provider
providers/node.js:61-63自己回 405(但 response 格式不同) - node provider 預設
enableJsonResponse: false,POST 會走 SSE 路徑 → Workers 30s CPU hang 回歸 - Claude re-initialize loop 重現
Fix approach
兩個方向(擇一或同時):
A. 加 production-wiring smoke test:在 built Nitro(preset cloudflare_module)下跑 POST /mcp initialize,斷言 Content-Type: application/json(非 text/event-stream)+ 完整 JSON-RPC response。確保 shim 真的被載入。
B. 把 fix 直接套到上游 @nuxtjs/mcp-toolkit cloudflare provider(PR 或 patch-package),取代 shim 這層。
A 輕量、快;B 根治但要維護 patch。建議先做 A 並登記 B 為長期計畫。
Acceptance
- 方案 A:
test/e2e/mcp-production-wiring.spec.ts或等效,在 build 後驗證 Content-Type + response 完整性。若 shim 被 bypass 則 test 立刻失敗 - 方案 B:上游 PR merged 或 patch-package 固定版本,shim 可 deprecated(但先保留避免 regression)
TD-030 — Claude.ai re-init 循環阻擋 tools/call(stateless 不足)
Status: done Priority: high Resolved: 2026-04-25 — wire-do-tool-dispatch v0.46.0 production flip + §6.4 4-layer fix(worker shim envelope inject / workerd HWM / nitro toNodeListener bypass / DO ReadableStream pattern)。staging acceptance 12/12 全綠,production worker fetch handler 正常驗證 bearer token 無 ownKeys/TypeError/5xx。 Discovered: 2026-04-24 — fix-mcp-streamable-http-session v0.37.0 post-deploy Claude.ai 實測 Location: MCP protocol layer(client=Claude.ai, server=server/mcp/index.ts + server/utils/mcp-agents-compat.ts) Related markers: @followup[TD-030] 在 openspec/changes/fix-mcp-streamable-http-session/tasks.md 5.2–5.5 / 6.1 / 6.2 / 6.4
Problem
fix-mcp-streamable-http-session v0.37.0 上線後實測:
- ✅
GET /mcp→405 Allow: POST,duration ~390ms(30s hang 消失) - ✅ 首次 handshake 全綠:
POST initialize 200 → notifications/initialized 202 → tools/list 200 - ✅ Claude.ai UI 顯示 "Loaded 4 Nuxt Edge Agentic RAG tools"
- ❌ 使用者按
AskKnowledge/SearchKnowledge/ListCategories→ UI 顯示 "Error occurred during tool execution" - ❌ wrangler tail 完全沒有
tools/callmethod 的 log
Tail 顯示的實際 pattern(使用者按任一 tool 之後):
POST /mcp initialize 400 (370ms) ← Claude 自發 re-initialize
GET /mcp 405 (390ms)
POST /mcp initialize 400
GET /mcp 405
... 每 3 秒循環,tools/call 從未送達 ...Claude 顯然把 GET 405 視為「stream 不可用 → 必須重建 session」,每次 tool call 前發新的 initialize。但第二次 initialize 的 body 被 MCP SDK transport 判為 invalid(可能是 Zod JSON-RPC schema parse fail 或 Server already initialized),回 400 → Claude 放棄 tool call。
這符合 design.md Fallback Plan 的情境:
"若 deploy 後 wrangler tail 觀察到 Claude.ai 對
405仍有異常行為(例如不接受 405、視為網路錯誤 retry),按序處理..."
實務上 Claude 不是「不接受 405」,而是「每次 tool call 前都要求 session 可繼續」。純 Workers stateless 模式無法滿足此期待。
Fix approach
按 design.md / proposal.md 原案 Fallback Plan 的方向 A:
開新 change upgrade-mcp-to-durable-objects,以 Cloudflare Durable Objects 承載 session state + SSE stream 重寫 MCP layer。具體 transport(agents/mcp McpAgent + WorkerTransport,或自寫 DO-backed transport 直接組 WebStandardStreamableHTTPServerTransport + DO storage)待 /spectra-discuss + spike 收斂 — 已知 blocker:server/utils/mcp-agents-compat.ts 註解記載 agents/mcp WorkerTransport 在 production tools/call 遇 Cloudflare proxy ownKeys error,MUST 先 spike 驗證是否在 DO context 仍發生。Tier 3 重工,新增 DO binding、新 class、deploy pipeline 改動。
搭配動作:
- Wire up
NUXT_KNOWLEDGE_FEATURE_MCP_SESSIONfeature flag(本 change 保留未用)讓 DO path 可漸進啟用 - Durable Object 承載 session state + server-initiated event 能力
- 保留
mcp-agents-compat.tsshim 的 GET 405 logic 作為 stateless fallback(例如 bearer-token-less probe) - 向 Anthropic 回報 Claude.ai 對 stateless MCP server 的 re-init 行為是否符合 MCP spec 2025-11-25 意圖
Acceptance
upgrade-mcp-to-durable-objectschange 上線後,Claude.ai 能穩定多輪 tool call(連續 3 次AskKnowledge不同 query 無 error banner)- wrangler tail 5 分鐘觀察:
tools/callmethod 正常出現,無POST initialize 400循環 GET /mcp回200 Content-Type: text/event-stream(DO 承載 SSE)或保留 405 — 具體策略隨 DO 設計決定- ChatGPT Remote MCP(若實測)同樣穩定
TD-040 — Token revoke 未同步清 MCP session Durable Object
Status: done Resolved: 2026-04-26 — add-mcp-token-revoke-do-cleanup change archived 於 openspec/changes/archive/2026-04-26-add-mcp-token-revoke-do-cleanup/。實作:(a) DO MCPSessionDurableObject.fetch() 開頭加 X-Mcp-Internal-Invalidate HMAC bypass → closeAllSseWriters + closeTransport + clearAllSseEvents + deleteAll + deleteAlarm;(b) DO initialize 寫 KV index mcp:session-by-token:<tokenId>;(c) admin revoke endpoint 加 cascadeInvalidateActiveSessions() best-effort cleanup(signing-key misconfig log.error 區分;KV/DO 失敗 swallow + log.warn)。HMAC trust anchor 沿用既有 NUXT_MCP_AUTH_SIGNING_KEY,無新 secret。已 ship v0.51.0 production deploy + smoke test 全綠。Evidence-based approval:integration spec 7/7 + unit 28/28 + 既有 DO lifecycle 22 tests + admin tokens route 9 tests 無 regression。 Priority: low Discovered: 2026-04-24 — upgrade-mcp-to-durable-objects Task 4.6 Location: server/api/admin/mcp-tokens/[id].delete.ts(revoke endpoint)+ server/durable-objects/mcp-session.ts(DO) Related markers: 本 change 完成後,tasks.md Task 4.6 保留 @followup[TD-040]
Problem
當管理員 revoke 一組 MCP token 時,該 token 已建立的 MCPSessionDurableObject session 不會被立即清除——僅依賴 DO 的 idle TTL alarm() 自然回收(預設 30 分鐘)。
影響:
- Token revoke 後 token holder 若已有 live session,仍可在 TTL 剩餘時間內繼續以舊 session 呼叫 tools(雖然 middleware 會攔 token,但 DO session 認同
Mcp-Session-Id而放行) - 稽核紀錄 revoke 時間與實際「停機」時間有差
- Low priority:實務上 revoke 後 middleware 的 bearer-token 驗證會 401 擋下(token hash 對不上),所以 session 實際無法觸發 tool call
Fix approach
需要 token → sessionId 索引(目前不存在)。兩種方案:
mcp_tokens表加active_session_ids TEXT[],issue token 時清空,每次 DOinitialize透過 admin API 把新 sessionId upsert 進來- KV 以
mcp:session-by-token:<tokenId>記錄 sessionId list,DOinitialize時寫入
Revoke endpoint 讀出 list → 迭代呼叫 env.MCP_SESSION.idFromName(sessionId).fetch(new Request('...', { method: 'DELETE' })) 讓 DO 清 storage。注意:目前 DO 把 DELETE 短路成 405,實作時要加一條 bypass(例如特定 X-MCP-Internal: invalidate header)或改用 POST /mcp/__invalidate。
Acceptance
- Admin UI revoke token 後
wrangler tail觀察:對應 sessionId 的 DO 立即deleteAll(或在 5 秒內) - 既有 DO TTL alarm 仍保留為 safety net
- Integration test:
token revoke → 同一 sessionId 的後續 request → 404
TD-041 — DO tool dispatch 未 wire up,flag=true 時 non-initialize 回假 ack
Status: done Priority: high Resolved: 2026-04-25 — wire-do-tool-dispatch v0.46.0 收尾。DO mcp-session.ts:handleGet/handleDelete/fetch 全鏈打通 + DoJsonRpcTransport 與 enqueueAndPushServerNotification 串接 + 4 tool handler dispatch 經 DO 路徑實際 work(staging acceptance 4 tool call isError=false 全綠 + production deploy 後 worker 正常 verify bearer token)。 Discovered: 2026-04-24 — upgrade-mcp-to-durable-objects Phase 4 C-path scope trim(pre-rollout review 發現 tasks 4.3 [x] 與實作不一致) Location: server/durable-objects/mcp-session.ts:158-181(假 ack path);server/durable-objects/mcp-do-transport.ts(DoJsonRpcTransport 已實作但未被 DO 呼叫) Related markers: @followup[TD-041] 於 openspec/changes/upgrade-mcp-to-durable-objects/tasks.md Task 4.3 / 4.3.1 / 5.2;由新 change wire-do-tool-dispatch 全面接手
Problem
upgrade-mcp-to-durable-objects Phase 4(Pivot C)完成 DoJsonRpcTransport class + MCPSessionDurableObject session lifecycle(create / touch lastSeenAt / alarm GC / 404 on missing),但 DO fetch() 對 non-initialize request(tools/list / tools/call / notifications/* 等所有 JSON-RPC methods)並未:
- 呼叫
DoJsonRpcTransport.dispatch(envelope, extra)把 message 丟給 SDK - Lazy init
McpServer+server.connect(transport)讓 SDK 處理 tool 派遣 - 從 Nuxt 層序列化 auth context (
event.context.mcpAuth) 並在 DO 內重建 - 從 DO env 取 Cloudflare bindings(D1 / KV / AI / BLOB)並注入 tool handler(目前 tool handler 透過
getCurrentMcpEvent()= NitrouseEvent()取得event.context.cloudflare.env,DO 內沒有 Nuxt H3Event)
Instead,non-initialize 目前回一個 { jsonrpc: '2.0', id, result: { session: { sessionId, lastSeenAt } } } 的假 ack。這意味著若 NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=true 在 production flip 為 true,Claude.ai 對每個 tools/call 會看到 HTTP 200 但 response body 與實際 tool 無關 — silent degradation,比 tool 直接 fail 更難察覺。
原 comment(server/durable-objects/mcp-session.ts:158-161)明確標示此為 follow-up:
Post-Phase 2 Pivot C: tool dispatch via
DoJsonRpcTransportlands in a follow-up task (context plumbing togetCurrentMcpEvent). For now the session renewal path returns a minimal JSON-RPC acknowledgement so session lifecycle is observable end-to-end.
Fix approach
兩段處理:
短期(本 DO change 完成前必做,避免 rollout 隱患):把假 ack 改為明確的 JSON-RPC error(code -32601 Method not found 或 -32603 Internal error),訊息指出「tool dispatch via DO 尚未實作,請設 NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=false 走 stateless fallback」。production flag 永不 flip 為 true 直到長期方案落地;staging 用來驗 session lifecycle。
長期(新 change wire-do-tool-dispatch 接手):
- DO 內 lazy init
McpServer並 register 4 個 tool(複製server/mcp/tools/*.ts的 definition 或重構為可注入 context 的 pure function) - Auth plumbing:Nuxt
/mcpmiddleware 驗完 bearer token 後,將McpAuthContext序列化為特殊 header(例如X-Mcp-Auth-Context,經 HMAC 簽章避免偽造)forward 到 DO;DO 內 parse + 重建 context - Env plumbing:DO env 已有 D1 / KV / AI / BLOB binding(同 worker 共用),但 tool handler 的
getCurrentMcpEvent()依賴 NitrouseEvent();需 fork tool handler 為(args, context: { env, auth, ... }) => ...形式,或在 DO 內建 shim event 滿足event.context.cloudflare.env/event.context.mcpAuth介面 Reflect.ownKeys(env)workaround:shim 層的installEnumerableSafeEnv要同步在 DO runtime 套用(DO 的 env proxy 是否重現同 bug 需實測)- Integration test:完整 e2e
tools/call → DO → McpServer → tool handler → retrieval → JSON-RPC response綠燈
Acceptance
短期 acceptance(本 DO change archive 前):
- [ ]
server/durable-objects/mcp-session.tsnon-initialize path 回 JSON-RPC error(error.code = -32601或-32603),body 包含「tool dispatch via DO not yet implemented」提示 - [ ]
test/integration/mcp-session-durable-object.spec.ts新增 assertion:non-initialize 時 DO 回error.code非 null - [ ] Production
NUXT_KNOWLEDGE_FEATURE_MCP_SESSION維持false;staging 僅用於 session lifecycle 驗證
長期 acceptance(wire-do-tool-dispatch change archive):
- [ ] DO 內
tools/call可完整執行 4 個 tool(askKnowledge / searchKnowledge / getDocumentChunk / listCategories)並回真實結果 - [ ] Auth context 從 Nuxt 安全傳到 DO(帶簽章、不被偽造)
- [ ]
wrangler tailproduction 3 次 tool call 無ownKeyserror、無 re-init loop、回應帶正確 content - [ ]
NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=true在 production flip 成功
TD-042 — Local NuxtHub dev KV binding 未注入 cloudflare.env → /mcp 503
Status: done Resolved: 2026-04-25 — 新增 server/utils/local-kv-bridge.ts 提供 wrapHubKvAsNamespace(unstorage → KVNamespace shape,含 TTL emulation:{ value, expiresAt } envelope + lazy 過期清除)+ bridgeLocalKvOnEvent(event-context 注入器);新增 server/plugins/local-kv-bridge.ts thin Nitro plugin,guard process.env.NUXT_KNOWLEDGE_ENVIRONMENT === 'local',hook request 時注入 event.context.cloudflare.env.KV。production / staging Workers runtime 不受影響(plugin 直接 return)。新增 test/unit/local-kv-bridge.spec.ts 7 tests 覆蓋 round-trip / TTL 邊界 / non-expiring / production guard / 既有 binding 保留。 Priority: mid Discovered: 2026-04-24 — add-mcp-tool-selection-evals task 5.2 apply(首次跑 pnpm eval 時發現) Location: server/utils/cloudflare-bindings.ts:46 (getCloudflareEnv) / server/utils/mcp-middleware.ts:113 (getRequiredKvBinding) Related markers: search @followup[TD-042] in repo
Problem
本專案 MCP middleware 在 rate-limit 階段呼叫 getRequiredKvBinding(event, 'KV'),該 helper 從 event.context.cloudflare?.env[bindingName] 或 globalThis.__env__[bindingName] 取 KV namespace。但 NuxtHub 的 local dev 用 fs-lite KV driver(.data/kv),並不把此 driver 注入 cloudflare.env — NuxtHub 官方的 local bridge 只處理 D1(見 node_modules/@nuxthub/core/dist/module.mjs:523),不處理 KV。
結果:local pnpm dev 的 POST /mcp 一定在 rate-limit lookup 時 throw 503 "Cloudflare KV binding \"KV\" is not available",任何經 middleware 的 MCP request 都走不完整個流程。影響:
pnpm eval(add-mcp-tool-selection-evals task 5.2)無法在 local 跑 baseline,暫時改指 staging/admin/tokensmint 的 dev MCP token 在 local 無法驗證(token auth 過了,KV 層炸)- 任何 integration test 以外(非 in-process stub)的 local MCP 測試都撞到這個
production / staging 真 Cloudflare Workers runtime 不受影響(KV binding 由 Workers runtime 注入 cloudflare.env)。
Fix approach
加一個 nitro server plugin(local-only guard),在 request lifecycle 最前面把 NuxtHub 的 hubKV() wrap 成 KVNamespace-shaped object,注入 event.context.cloudflare.env[bindingName]:
typescript
// server/plugins/local-kv-bridge.ts
export default defineNitroPlugin((nitroApp) => {
if (process.env.NUXT_KNOWLEDGE_ENVIRONMENT !== 'local') return
nitroApp.hooks.hook('request', (event) => {
event.context.cloudflare ??= { env: {} }
event.context.cloudflare.env.KV ??= wrapHubKvAsNamespace(hubKV())
})
})wrapHubKvAsNamespace 只要實作 get(key): Promise<string|null> 與 put(key, value, { expirationTtl? }),match KvBindingLike interface(server/utils/cloudflare-bindings.ts:21)。也需要寫對應 unit test 覆蓋 expire TTL round-trip。
替代路線(較大改動但更根本):把 cloudflare-bindings.ts 的 getCloudflareEnv 改為對 KV 優先走 hubKV() → 不需 wrap 每個 request。但這讓 util 同時支援兩種 runtime,更難推理;偏好 plugin 方式讓 call site 不動。
Acceptance
- Local
pnpm dev下,用有效 Bearer tokencurl -X POST http://localhost:3010/mcp可走到tools/list並回 JSON-RPC success(不再 503) - Rate-limit 記錄實際寫到
.data/kv/,第 N+1 次請求被擋(fixed-window 驗證) add-mcp-tool-selection-evals可把EVAL_MCP_URL切回http://localhost:3010/mcp跑 baseline,且分數與 staging baseline 差異 ≤ 5pp(驗證行為一致)- 新增
test/integration/local-kv-bridge.spec.ts覆蓋wrapHubKvAsNamespace的 get / put / ttl 行為
TD-043 — Evalite afterAll 的 process.exit / throw 不 propagate 到 pnpm eval
Status: done Resolved: 2026-04-25 — 採方案 2 wrapper script:新增 scripts/run-eval-with-exit.mjs(pure Node ESM,無第三方依賴;spawn pnpm exec evalite run test/evals/mcp-tool-selection.eval.ts + 即時 pipe stdout/stderr + buffer 累積;child exit 後若 combined buffer.includes('Eval regression:') 且 child 0 → exit 1,child non-zero → 保留 child code;SIGINT/SIGTERM forward 給 child)。package.json 的 eval script 改 node scripts/run-eval-with-exit.mjs(eval:report / eval:watch 不變)。新增 test/unit/run-eval-with-exit.spec.ts 4 cases 覆蓋 decideExitCode 純函式(child 0 + 無/有 banner、child non-zero 保留)。test/evals/mcp-tool-selection.eval.ts 不動,wrapper 依賴既有 stderr banner。Acceptance(實際跑 eval 觸發 regression)留待使用者執行(會打 Anthropic API + AutoRAG,不可重現)。 Priority: low Discovered: 2026-04-24 — add-mcp-tool-selection-evals task 6.5 驗證(故意改壞 dataset 讓 overall < baseline − 5pp 時,stderr 有印 regression banner,但 pnpm eval 仍以 exit code 0 結束) Location: test/evals/mcp-tool-selection.eval.ts afterAll block,evalite@0.19.0 runtime Related markers: search @followup[TD-043] in repo
Problem
Design Decision 5 要求 overall score < baseline − 5pp 時 pnpm eval 以 non-zero exit code 結束(nightly / manual 回饋用)。實作嘗試了三條路線:
process.exitCode = 1(afterAll)— evalite / vitest 不讀throw new Error('regression...')(afterAll)— 被 vitest / evalite 內部 wrapper catchconsole.error(...) + process.exit(1)(afterAll)— banner 在 stderr 出現、但 pnpm 仍看到 exit code 0(evalite 似乎 spawn child 並吃掉 exit code,或 vitest 的afterAll被 wrap)
三種方案 stderr signal 都有,但 exit code 都沒 propagate 到 pnpm / shell。目前 regression 只能靠 CI 腳本 grep log 的 Eval regression: 字串偵測,不是 Decision 5 描述的「exit non-zero」。
Fix approach
三條可能的 fix:
- 改用 evalite 原生 fail 機制:如果 evalite 有 scorer-level threshold(例如
{ threshold: 0.5 }讓 eval 標 failed),改把 regression 判斷放到個別 sample scorer 內,回傳score: 0+metadata.regression: true,讓 evalite 自身決定 exit。需查 evalite 0.19 支援情況 - Wrapper script:
package.json的evalscript 改成node scripts/run-eval-with-exit.mjs,wrapper 跑 evalite 後 grep stdout 的Eval regression:字串,match 就 exit 1。簡單、與 evalite 版本解耦、但多一層 script - 升 evalite 版本:檢查 0.20+ 是否修掉 afterAll throw swallow;若有直接升版
Acceptance
- 故意改壞 dataset(overall < baseline − 5pp)後
pnpm eval; echo $?印 非 0 - 正常 dataset(overall ≈ baseline)後
pnpm eval; echo $?印 0
TD-044 — session.create.before 靜默吞 user_profiles UNIQUE 衝突 → better-auth user id 與 user_profiles.id 可能漂移
Status: done Resolved: 2026-04-25 — fix-user-profile-id-drift change archived;session.create.before hook 改寫為 email_normalized-first lookup + app-level migrate children + non-production rethrow + actionable log hint,8 unit tests + live verify 全綠 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(local dev 發訊息 → /api/chat 500 FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEY) Location: server/auth.config.ts:487-513 (session.create.before hook 的 user_profiles sync try/catch) Related markers: search @followup[TD-044] in repo
Problem
better-auth 在 session.create.before hook 內 upsert user_profiles row(見 server/auth.config.ts:492):
typescript
try {
await hubDb
.insert(schema.userProfiles)
.values({
id: session.userId,
emailNormalized,
roleSnapshot: finalRole,
adminSource,
})
.onConflictDoUpdate({...})
} catch (error) {
authLog.error('user_profiles sync failed', {...})
// 繼續,不 rethrow
}user_profiles.email_normalized 有 NOT NULL UNIQUE(見 migration 0001_bootstrap_v1_core.sql),但 onConflictDoUpdate 的 target 是 PRIMARY KEY (id),不是 email_normalized。若資料庫中已有同 email_normalized 但不同 id 的 stale user_profiles row(通常因 better-auth user 表曾被手動刪 + 重建產生新 id),hook 會撞 UNIQUE(email_normalized) conflict → catch 吞掉 → 新 better-auth user 的 id 在 user_profiles 裡根本沒對應 row。
後果:任何依 conversations.user_profile_id / query_logs.user_profile_id / messages.user_profile_id FK 的寫入都會 500:
D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEY
at Object.createForUser (conversationStore)
at Object.chatHandlerProduction Cloudflare D1 若沒發生過 user 刪除重建不會中鏢,但 local dev(多次 _dev/login / 手動 reset DB)很容易卡到 stale row,螢幕是詭異的「明明登入成功但 /api/chat 一直 500」。人工檢查 / screenshot review / 任何依 /api/chat 的 E2E 都會隨機失敗,且錯誤訊息「Chat failed」沒揭露 FK 真因。
Fix approach
三個選項(建議 1 + 3 雙保險):
- 改
onConflictDoUpdate的 conflict target:從 id → email_normalized,配合SET id = excluded.id。這會讓 UNIQUE 衝突時改成「更新既有 row 的 id 指到新 user」,自然把 stale 接管。但要注意 children FK(conversations / messages / query_logs)需要 ON UPDATE CASCADE 才跟著動,否則變孤兒。 - 或先查後寫:插入前
SELECT id FROM user_profiles WHERE email_normalized = ?,有 stale → 決定是 update id(+ cascade children)還是 delete stale 再 insert;無則直接 insert。邏輯多但可控。 - 讓 catch 重新 throw(至少 in non-production):
if (process.env.NODE_ENV !== 'production') throw。local 立刻 fail fast,生產保留現有保守行為。搭配 1/2 後這條幾乎不會炸,但若未來再有 drift 會立即暴露。
同時補:
- log.error 輸出後加個 actionable hint(「可能是 user_profiles 有 stale row 指向舊 better-auth user;檢查 email_normalized = X 的既有 row」),省下次 debug 的時間。
- migration 層面的 TD-009(
email_normalized改 nullable)不直接修本 bug,但配合 unique index 若加上 partial index (WHERE email_normalized IS NOT NULL) 可同時化解 passkey-only user 佔位衝突。
Acceptance
- Local
rm .data/db/sqlite.db && rm -rf .wrangler/state/v3/d1/...rebuild 後首次/_dev/login+ 發訊息 /api/chat 200 - 手動
DELETE FROM user; INSERT INTO user (id, email, ...) VALUES ('new-id', same-email, ...)(或等同 better-auth 刪重建)後再登入 + 發訊息 /api/chat 200(不再 500) - 新增 unit 覆蓋:stub
hubDb.insert模擬 UNIQUE email_normalized 衝突,hook 走「改 id 路徑」把 user_profiles row 指到新 id,後續 conversations insert 不 FK fail - auth.config.ts 的 catch 在 non-production 改為 rethrow(或至少:error.log 有明確「stale user_profiles row 可能在 email=X」的 actionable hint)
TD-045 — Local dev bootstrap 連串斷點
Status: in-progress Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(新 session pnpm dev 後 /api/chat 一路從 500 FK error → 503 binding 未設 → 503 AutoRAG not found,共三關) Location: scripts/check-dev-bootstrap-health.mjs(predev 警告)、docs/tech-debt.md(本 entry)、待動:[nuxt-hub] DB binding not found 間歇 500 trace 定位 Related markers: search @followup[TD-045] in repo
Problem
Local dev 起床到 /api/chat 200 的路徑曾經有三條坑(原始發現時列入),經 v0.43.2 前後實測後收斂:只剩「.env NUXT_KNOWLEDGE_AI_SEARCH_INDEX 空值 → /api/chat 503」為穩定可重現,其餘兩條在目前 NuxtHub v0.10.7 路徑下不會出現。此外截圖審查過程浮現新症狀:/api/auth/me/credentials 間歇性 500 [nuxt-hub] DB binding not found,疑似 miniflare D1 binding cold-start / HMR race,未定位前歸入本 entry 的 active scope。
Status Update(2026-04-25, post-v0.43.2)
原始 Problem 三條坑經實測後收斂為 narrow scope:
- Problem #1(NuxtHub 不 auto-apply migrations):
obsolete→ regressed/重新發現 @ 2026-04-25fix-user-profile-id-driftapply 階段 cleanroom 實戰。先前 entry 描述「v0.10.7 已自動 apply」實際為 false:node_modules/@nuxthub/core/dist/db/runtime/plugins/migrations.dev.mjs內 dev plugin 仍 gated byif (!hub.db.applyMigrationsDuringDev) return;,nuxt.config.tshub: { db: 'sqlite' }沒設此 flag,所以 cleanroom 後_hub_migrations僅建立空表、所有 migration 都未 apply。原本 entry 觀察到「11 筆應用紀錄」的環境是 v0.10.6 或更早跑過 migrate 後留存的狀態,cleanroom 重做時退回真空。Reproduction:rm .data/db/sqlite.db && rm -rf .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev,第一次POST /api/_dev/login500Failed to prepare credential account(drizzle 對未存在的user表 SELECT 失敗)。手動for f in server/database/migrations/*.sql; do sqlite3 .data/db/sqlite.db < $f; done可灌進 schema 但 0007 ALTER 路徑跟 NuxtHub plugin 不對齊(產出account.userId REFERENCES user_new(id)殘留)。需 opt-inapplyMigrationsDuringDev: true(簡單)或上游將其改 dev 預設值(需 NuxtHub PR)。 - Problem #2(stale
*_newFK refs):not-reproduced。當前 DB 的sqlite_master無任何*_new(…)殘留,PRAGMA foreign_key_check;乾淨。此 bug 只會在手動sqlite3 < migration.sql路徑出現,正常 NuxtHub auto-apply 不會踩。v0.43.2 的check-dev-bootstrap-health.mjs已加 defensive 偵測(若 regresses,predev 會印 actionable warning + rebuild 指令)。 - Problem #3(
AI_SEARCH_INDEX=空值):partial fix shipped @ v0.43.2 (00e5314)。predev現會 warnNUXT_KNOWLEDGE_ENVIRONMENT=local+NUXT_KNOWLEDGE_AI_SEARCH_INDEX=空值的組合,並指引 Notion Secret 頁「Local chat / AutoRAG 驗證指引」。script exit 0 不擋 dev。 - 新增 active scope(從 HANDOFF Blocked 繼承):
/api/auth/me/credentials間歇性 500[nuxt-hub] DB binding not found— 原 TD-045 未涵蓋,截圖審查時才浮現;需要可重現 trace 才能定位(疑似 miniflare cold-start race),本輪未碰。
Fix approach(remaining)
- NuxtHub dev migration auto-apply opt-in —
nuxt.config.tshub: { db: 'sqlite', applyMigrationsDuringDev: true }。最小成本,立刻讓 cleanroom 流程可跑。權衡:每次 dev startup 多跑 migration(idempotent,影響 < 1s),但 dev plugin 內部 try/catch 邏輯如果 migration 損壞會把錯誤帶到 startup(vs 目前是 lazy fail at request time)— 看做 fail-fast 優點。 [nuxt-hub] DB binding not found間歇 500 定位 — 收集一次重現 trace(timestamp / 其他同時請求 / miniflare 啟動 log)。可能方向:miniflare D1 binding hydration 的 race、HMR 後 server context 未重新綁定、@nuxthub/corev0.10.7 vs 舊版行為差異。.env.example加註解 — 目前被guard-checkpermanent-protected。若之後要動,使用者需手動改或解 guard;script output 已涵蓋指引,優先級低。- cleanroom e2e 驗證 — Fix #1 後,
rm -rf .data/db .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev跑一次,確認全程無手動 sqlite3 步驟即可到 /api/chat 200。同時追跑fix-user-profile-id-drifttask 7.1 / 7.2 / 9.1 / 9.2(其 markers@followup[TD-045]已登記)。
Acceptance
- [x]
sqlite3 .data/db/sqlite.db "SELECT count(*) FROM sqlite_master WHERE sql LIKE '%_new(%'"回 0(驗證 2026-04-25) - [x] 若
.env未設NUXT_KNOWLEDGE_AI_SEARCH_INDEX,pnpm dev終端機清楚提示下一步(scripts/check-dev-bootstrap-health.mjs實作,v0.43.200e5314) - [ ]
nuxt.config.tshub.db.applyMigrationsDuringDev: true(或 NuxtHub upstream 把 dev 預設改 true) - [ ]
rm -rf .data/db .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev後首次POST /_dev/login+/api/chat直接 200(2026-04-25 實戰驗證為 500,根因 Problem #1 regression,等 opt-in 後重跑) - [ ]
fix-user-profile-id-drifttask 7.1 / 7.2 / 9.1 / 9.2 在 cleanroom 修好後追跑通過 - [ ]
/api/auth/me/credentials間歇性 500 已定位並修復(需 trace)
TD-046 — agentic-rag-staging AutoRAG index 在 CF 帳號中不存在
Status: done Resolved: 2026-04-25 — 透過 CF API 建 agentic-rag-staging AutoRAG(複製 production config,source agentic-rag-documents-staging,embedding @cf/qwen/qwen3-embedding-0.6b,sync_interval 21600s)+ 同名 AI Gateway。wire-do-tool-dispatch §7.1 4 個 tool call (askKnowledge x2 / searchKnowledge x2) 全部回 isError: false(empty results / refused 為 staging R2 空所致正常行為)。Staging R2 seed sample docs / production sync 屬於後續 RAG content 議題,不在本 TD scope 內,需要時另開 TD。 Priority: high Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(local /api/chat 503 AutoRAG not found;CF API 驗證) Location: wrangler.staging.jsonc、.github/workflows/deploy.yml:238、Notion Secret 頁、agentic-rag-staging binding consumer Related markers: search @followup[TD-046] in repo
Problem
多個 config source 都宣稱 staging 環境用 agentic-rag-staging AutoRAG index:
wrangler.staging.jsoncNUXT_KNOWLEDGE_AI_SEARCH_INDEX=agentic-rag-staging.github/workflows/deploy.yml:238NUXT_KNOWLEDGE_AI_SEARCH_INDEX: agentic-rag-staging- Notion Secret 頁「環境總覽」staging row
Search index: agentic-rag-staging wire-do-tool-dispatch/upgrade-mcp-to-durable-objects之 immediate validation 都假設 staging AutoRAG 能用
但 Cloudflare API GET /accounts/{account_id}/autorag/rags 實際只回一筆:agentic-rag(production)。agentic-rag-staging 在 Dashboard / API 皆不存在。
可能原因:
- 規劃時寫入 config 但忘了在 CF 建
- 曾經有、後來刪但 config 沒清
- 建 staging AutoRAG 成本 / 資料 ingest 門檻讓團隊暫緩,但沒 flag 出來
影響:
- Staging deployed worker 一旦被 end-to-end 測(真正 call /api/chat),會在 AutoRAG search step 503「AutoRAG not found」
- MCP
askKnowledge/searchKnowledge在 staging 也會踩同樣錯 - Local dev 若照 Notion 指引指向 staging,同樣被擋
wire-do-tool-dispatch的 staging immediate validation(HANDOFF Next Steps 2-3)可能也 partial pass only —tools/list過了但真askKnowledgetool call 會死在 AutoRAG
Fix approach
兩個方向擇一:
- 建
agentic-rag-stagingAutoRAG index:到 CF Dashboard → AI → AutoRAG → 建 RAG,sourceagentic-rag-documents-staging(R2 bucket 已存在)、embedding / chunk size 與 production 對齊、AI Gateway 指agentic-rag-staging(若 gateway 也沒建則同步建)。完成後執行 ingest(staging 資料集可以是 production 的子集)。 - 放棄 staging AutoRAG,把 staging 指向 production index:修
wrangler.staging.jsonc/ deploy.yml / Notion 讓NUXT_KNOWLEDGE_AI_SEARCH_INDEX=agentic-rag。風險:staging 讀到真實客戶資料,cache 命中 / 額度共用 production;需 risk review 是否 acceptable。
建議走 1(正確做法),時間壓力下走 2 並明確記錄。
Acceptance
- CF API
GET /accounts/{acc}/autorag/rags回傳包含agentic-rag-staging(若走方向 1) - 或 staging worker 部署後
curl -X POST .../api/chat不再 503 AutoRAG not found wire-do-tool-dispatch的 immediate validation(HANDOFF §2.3)askKnowledge/searchKnowledgetool call 真的拿到 citation 不是 error
TD-047 — /api/chat SSE ready 後階段 error 時 Container 未 emit conversation-persisted
Status: done Resolved: 2026-04-25 — app/utils/chat-stream.ts 抽出 ReadChatStreamInput interface 並新增 onReady?: (data) => Promise<void> | void callback,handleEventBlock 的 case 'ready' 改為呼叫 await input.onReady?.(event.data) 後仍 return null(不阻 stream、不改 terminal flow)。app/components/chat/Container.vue 加 pendingConversation slot,submit 開始 reset、ready callback 寫入、catch block 在 kind !== 'abort' && pendingConversation 時 emit conversation-persisted fallback、finally 清除。abort 路徑刻意不 emit fallback(使用者主動取消不應 lock active id)。test/unit/chat-stream.test.ts +2 cases、test/unit/chat-container-streaming-contract.test.ts +3 cases 覆蓋 onReady invocation / ready-then-error path / fallback emit / submission 間 reset。 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 debug Location: app/components/chat/Container.vue:228(try block emit 路徑)、app/utils/chat-stream.ts:146(ready event discard) Related markers: search @followup[TD-047] in repo
Problem
/api/chat 的 SSE 流程:
- Server 先把 conversation insert 到 DB(
conversationStore.createForUser) - 回傳
event: ready data: {conversationCreated: true, conversationId: ...} - 接著跑
chatWithKnowledge(AutoRAG search、Workers AI 生成、judge…) - 若 step 3 任何環節 throw → enqueue
event: error
目前 Container.vue 的 client 實作:
readChatStream的readycasereturn null(discard)complete/refusalcasereturn event→ Container 從terminalEvent.data.conversationId讀 id 並emit('conversation-persisted', ...)errorcasethrow new ChatStreamError→ Container 走 catch block,不 emit
結果:DB 已經建了 conversation,但 UI 不知道;active conversation id 保持 null、sidebar historyRefreshKey 不 bump、使用者下一次發訊息又被當新對話 → DB 出現孤兒 conversation,user 只能 reload 才看得到。TD-046 的 staging AutoRAG 問題直接暴露這條 UX 缺口。
Fix approach
兩個層次一起修:
chat-stream.ts:readyevent 不要 discard,把conversationCreated/conversationId存進一個 handler ref。讓readChatStreamsignature 多一個onReady?: (data) => voidcallback 或在 return type 加earlyReady: {...}欄位。Container.vue:- onReady callback 時先存
pendingConversation = {id, created} - 成功路徑繼續照舊 emit(拿 terminalEvent 的值)
- catch block 裡若
pendingConversation非空,用它 emitconversation-persisted(標記 message 為 error placeholder),讓 page / history 至少能 refresh active + sidebar
- onReady callback 時先存
- 或者更簡單:server side 把 conversation create 從 handler 搬到
execute()成功之後,避免孤兒 conversation。但這會失去 "streaming starts before DB write" 的 latency 優勢,需要 trade-off review。
Acceptance
- 故意把 AutoRAG search 拔掉(例如 env AI_SEARCH_INDEX 指不存在的 index),發訊息後:
- Sidebar 立刻出現新 conversation(或 Container 明確呼叫 DELETE /api/conversations/{id} 清 orphan)
- 下一次發訊息會帶 conversationId 而不是建新 conv
- Unit test:stub
readChatStream模擬 ready-then-error,驗證 Container emitconversation-persisted且帶正確 id
TD-048 — 聊天 UI 缺顯式「新對話」入口
Status: done Resolved: 2026-04-25 — add-new-conversation-entry-points change archived;chat header 新對話按鈕 + reload 行為修復 + Safari private mode 相容(QuotaExceededError catch)+ e2e 5/5 + Design Review 完成 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.2 人工檢查(使用者反映「找不到新對話按鈕」、「reload 始終停在同個對話」) Change: add-new-conversation-entry-points(archived 2026-04-25) Location: app/pages/index.vue(無明顯 CTA)、app/components/chat/ConversationHistory.vue:129 的 + icon 實際只 requestExpand、app/layouts/chat.vue header 也無新對話按鈕 Related markers: search @followup[TD-048] in repo
Problem
Chat UI 沒有任何顯式的「新對話 / New chat」按鈕或 menu item:
- ConversationHistory.vue L129 的
i-lucide-plus按鈕 aria-label"新增對話"只在收合的 sidebar 出現,@click="requestExpand"只做展開 sidebar 的事,不真的重置 active(容易誤會) - 展開狀態 sidebar 沒有 + 按鈕
- 頂部 nav 的「問答」link
to="/"在已有 active 的狀況下 navigate 到/不會清掉 sessionStorage 裡的 active id,restoreActiveConversation會把它讀回來 - 使用者唯一的「開新對話」路徑是:刪除當前 active conversation,或手動清
sessionStorage['web-chat:active-conversation:<userId>']後 reload
人工檢查時使用者反映「我找不到新對話按鈕」、「整頁重整始終在同個對話」— 屬於正當 UX 需求被封死。這不只阻擋 review flow,也會讓真實使用者卡住(想新開一個主題的對話就做不到)。
Fix approach
三個層次擇一 / 並用:
- 明顯的 CTA:在展開 sidebar 的 header 右側加
UButton icon="i-lucide-plus" aria-label="新對話",click → 呼叫handleConversationCleared()(index.vue 已有這 handler,做的事就是setActiveConversation({conversationId: null, messages: []}))。成本最低、語意最清楚。 - Header-level 入口:chat layout header 加一個「新對話」按鈕,mobile / desktop 都能看見;click 路徑同上。
- 「問答」nav link 行為修正:若 active 非 null 且 click
/,視為清空 active 的 intent(也要清 sessionStorage)。比較隱晦,不建議單用,但可作為輔助。
額外 UX 考量:clear 的 confirm flow — 若使用者誤點要不要有 undo / 雙層確認?目前 active 被切換時就 drop 使用者剛打字未送出的 input;需補一層 dialog 或改 setActiveConversation 不清 draft。
Acceptance
- 展開 sidebar 可見「新對話」按鈕(desktop + mobile drawer 都有)
- 按下後主面板回到 hero empty state、sidebar active 高亮消失、sessionStorage 內
web-chat:active-conversation:<userId>被刪 - reload 後不會把剛開新對話的狀態覆寫回舊的
- 新增 E2E / unit test 覆蓋「建 conv A → 按新對話 → 建 conv B,兩筆各自獨立」
TD-049 — Cloudflare Pages deploy API 拒絕 git HEAD commit message
Status: done Resolved: 2026-04-26 — workaround commit 5ce334c(顯式傳 --commit-message "Deploy <SHA>" 與 --commit-hash "<SHA>",其中 <SHA> 為 GitHub Actions github.sha context 的 expression 寫法,詳見 .github/workflows/deploy.yml)落地後,至 v0.50.1 ship 為止 GitHub Actions Deploy workflow 連續 30 條 run 全綠(過去最近抽查 gh run list --workflow=Deploy -L 30 -- --jq 'select(.conclusion=="failure")' = 0 hits),workaround 已驗證對普遍 commit message 有效。原 acceptance 第 2 條(v0.43.0 manual rerun)省略 — 後續 30+ 次 deploy 已遠超 acceptance 第 3 條的「3 次穩定」門檻。 **Priority**: mid **Discovered**: 2026-04-25 — v0.43.0 release deploy run [24908303837](https://github.com/YuDefine/nuxt-edge-agentic-rag/actions/runs/24908303837) **Location**: .github/workflows/deploy.ymldeploy-docs-production/deploy-docs-staging的Deploy docs to Cloudflare Pagesstep **Related markers**: search@followup[TD-049]` in repo
Problem
wrangler pages deploy 在不顯式傳 --commit-message 時會把 git HEAD commit subject + body 附到 Cloudflare Pages API 的 deployment record。對某些 commit message,Cloudflare API 會回:
✘ [ERROR] A request to the Cloudflare API (/accounts/.../pages/projects/.../deployments) failed.
Invalid commit message, it must be a valid UTF-8 string. [code: 8000111]導致 deploy-docs-production / deploy-docs-staging 失敗,docs 站(agentic-docs.yudefine.com.tw)停留在前一版。app worker deploy 不受影響(走 wrangler deploy --config server/wrangler.staging.json,不經 Pages API)。
重現條件(2026-04-25 觀察)
| Commit | Type | Docs Pages deploy | 特徵 |
|---|---|---|---|
5a47a63 v0.43.0 tag | tag push | ❌ 8000111 | emoji + 繁中 + 全形括號 + 頓號 + TD-044~048 |
a0e2426 更新 HANDOFF | main push | ✅ | 也是 emoji + 繁中 + 全形標點 + 美式引號 |
兩個 commit message 經 git log --format='%s%n%b' <sha> \| xxd 檢驗皆為合法 UTF-8,所以 Cloudflare Invalid UTF-8 string 的錯誤訊息本身不精確——非 wrangler 4.84.1 全面漂移,也不是本機 encoding 問題。觸發條件疑為 CF API 某個未文件化的 validator,可能針對:
- 特定字元組合(例如
TD-044~048的 ASCII tilde + 全形括號) - body 的特定格式(換行 + 前導空白 + list marker 的組合)
- body 長度或某個 byte sequence
Fix approach
Short-term (this TD) — .github/workflows/deploy.yml 的兩個 deploy-docs-* step 顯式傳:
yaml
# 下列 <SHA> 為 GitHub Actions context expression `github.sha`(實際 yaml 寫法
# 為 dollar 符 + 雙大括號包 github.sha;此處改用占位符,避免 docs site 的
# Vue SSR compiler 把字面 mustache 誤判為模板表達式)。
run: pnpm exec wrangler pages deploy docs/.vitepress/dist \
--project-name "$DOCS_CF_PAGES_PROJECT_NAME" \
--branch "$DOCS_CF_PAGES_*_BRANCH" \
--commit-hash "<SHA>" \
--commit-message "Deploy <SHA>"實際 GitHub Actions expression 以 .github/workflows/deploy.yml deploy-docs-* job 為準。避開 git HEAD commit message 直接作為 CF API 輸入。--commit-hash 仍保留 SHA 供 CF dashboard 顯示。
Long-term — 若此問題在其他專案重現、或未來要恢復用 git commit message(保留 dashboard 可讀性),整理:
5a47a63完整 body bytes(已在docs/solutions/tooling/2026-04-25-cloudflare-pages-utf8-commit-message.md)- 一個最小 repro(4 個字元的 commit message 還會觸發嗎?)
- 開 issue 於
cloudflare/workers-sdk或 CF Pages support
Acceptance
- [x]
.github/workflows/deploy.yml兩個deploy-docs-*job 的wrangler pages deploy帶--commit-message+--commit-hash(commit5ce334c) - [x]
重跑 v0.43.0 docs production deploy— 省略,後續 30+ deploy run 已驗 workaround 普適 - [x] 後續 3 次 main push / tag 發版,
deploy-docs-*皆綠(實際達 30+ 次連綠) - [ ] (Optional)保存
5a47a63完整 bytes + 最小 repro,供未來向 workers-sdk / CF 回報
TD-050 — Production / staging demo seed evidence bridge
Status: done Resolved: 2026-05-04 — 新增 scripts/demo-seed/ + pnpm demo-seed <staging|production>,並已套用到 staging 與 production。兩個環境都有 12 份 demo documents、14 個 versions、94 個 source chunks、12 筆 citation_records、16 筆 query_logs、18 筆 messages、5 個 demo users、4 個 MCP token rows;R2 寫入 108 個 demo objects;AI Search sync job 完成(staging ae14e71d-d066-45e5-87b2-e44c41a9f3d4、production 20d8bc95-e45f-44a0-99e7-1318c1fea922)。pnpm demo-seed <env> --verify-only 透過 Worker AI binding structured filters 驗證 internal / restricted 查詢各回 5 筆 active/current 且含 document_version_id / citation_locator / access_level metadata 的 chunks。 Priority: mid Discovered: 2026-04-25 — wire-do-tool-dispatch §7.1 post-fix observation(TD-046 修復後 4 個 tool call isError: false 但 citations:[] / results:[] empty) Location: scripts/demo-seed/, docs/runbooks/demo-seed.md, R2 buckets agentic-rag-documents-staging / agentic-rag-documents, AI Search instances agentic-rag-staging / agentic-rag, D1 agentic-rag-db-staging / agentic-rag-dbRelated markers: search @followup[TD-050] in repo
Problem
TD-046 修復後 staging agentic-rag-staging binding 已可正常呼叫。2026-04-25 已上傳 5 份 fixture 到 R2;2026-05-04 live audit 進一步確認 wrangler ai-search stats agentic-rag-staging 顯示 Indexed=5、R2 bucket object_count=5 / bucket_size=18 kB。
但 staging app 仍無法當成完整 RAG demo 環境,因為 D1 agentic-rag-db-staging 當時沒有對應文件中繼資料:
documents=0document_versions=0source_chunks=0query_logs40 筆中 33 筆是decision_path='no_citation_refuse'
目前直接打 wrangler ai-search search agentic-rag-staging 可以查到 staging-seed/procurement-manual.md,但結果沒有 app 所需的 document_version_id / citation_locator metadata;而 app retrieval pipeline 會在 AI Search 後做 D1 post-verification。缺 D1 evidence bridge 時,所有 staging RAG retrieval 仍會回 empty / refused:
askKnowledge→{"citations":[],"refused":true}(無 evidence ⇒ refused,符合 retrieve-then-answer 設計)searchKnowledge→{"results":[]}(無 D1 verified evidence ⇒ empty hits)
對於 wire-do-tool-dispatch §7.1 immediate validation 而言,這仍滿足「非 501 / 非 re-init / isError: false」門檻,不阻擋 archive;但若使用者:
- 走 staging 做端到端 chat 體驗測試
- 從 local dev 指向 staging 跑
pnpm eval - 想用 staging 重現 production RAG 行為
都會發現實質上「拿不到答案」。
2026-05-04 已改成 production / staging 共用的 demo seed contract,並補齊 app 所需的 D1 evidence bridge 與 R2 chunk metadata。Production 也同步擴充,避免正式展示環境只有採購流程與少量 smoke docs。
Fix approach
已落地:
scripts/demo-seed/demo-seed-data.ts定義 deterministic seed contract 與 feature coverage。scripts/demo-seed/demo-seed-worker.ts用 remote Wrangler worker 寫入正式 D1 / R2 binding,R2 chunk object 帶status、version_state、access_level、document_version_id、citation_locator。scripts/demo-seed/run-demo-seed.ts封裝 dry-run、apply、AI Search sync、AI binding structured-filter verification。docs/runbooks/demo-seed.md記錄重跑與驗證方式。
Acceptance
- [x] Staging / production D1 具備 demo 文件:各
documents=12、document_versions=14、source_chunks=94、current versions11。 - [x] Staging / production R2 具備 demo source + normalized chunks:各 108 個 demo objects;末端 chunk
normalized-text/demo-<env>-ver-legacy-procurement-2024-v1/0006.txt可下載。 - [x] AI Search sync 完成:staging job
ae14e71d-d066-45e5-87b2-e44c41a9f3d4、production job20d8bc95-e45f-44a0-99e7-1318c1fea922。 - [x] Worker AI binding structured filters 驗證:internal 查詢「PR 和 PO 的差別」與 restricted 查詢「restricted access 可以查哪些預算資料」在兩個環境各回 5 筆 active/current chunks,且 metadata 完整。
- [x] Seed 來源與維護方式記錄於
docs/runbooks/demo-seed.md。
Progress
2026-04-25:已用 wrangler r2 object put --remote 上傳 5 份 static fixture 至 agentic-rag-documents-staging/staging-seed/:
README.mdprocurement-manual.mdhr-policy.mdsystem-faq.txtprocurement-manual-v2.md
2026-05-04 live audit:agentic-rag-staging AI Search 已有 Indexed=5,但 D1 agentic-rag-db-staging 仍是 documents=0 / source_chunks=0;staging search 對「採購流程操作手冊」可命中 staging-seed/procurement-manual.md,但對「員工請假辦法」、「差旅費用報銷」、「新人入職指南」也只回同一份 procurement 檔,且無 metadata。尚未關閉本 TD:下一步不是單純等 indexing,而是把 staging fixture 補進 app 的 D1 evidence / current-version 鏈,再重跑 tool call 驗證。
2026-05-04 resolution:已補 production / staging 共用 demo seed,並在兩個環境驗證 D1 / R2 / AI Search / HTTP:
- D1:兩環境各
documents=12、active_documents=11、archived_documents=1、versions=14、current_versions=11、chunks=94、citations=12、query_logs=16、messages=18、conversations=5、tokens=4、users=5、role_changes=4。 - R2:兩環境末端 normalized chunk 可下載,內容為封存文件稽核用途片段。
- AI Search binding:兩環境 internal / restricted probe 都只回 active/current chunks,且包含 app 所需 citation metadata。
- HTTP:
https://agentic.yudefine.com.tw與https://agentic-staging.yudefine.com.tw皆回 200。
TD-051 — libsql legacy_alter_table=1 破壞 0007/0009 的 RENAME-rewrite 假設
Status: done Priority: high Discovered: 2026-04-25 — dev server 報 unable_to_link_account(better-auth Google OAuth 登入路徑) Location:
- 根因:
server/database/migrations/0007_better_auth_timestamp_affinity.sql:464-471、server/database/migrations/0009_passkey_and_display_name.sql:116-218的 RENAME-rewrite 假設 - 修正:
server/database/migrations/0012_fk_rebuild_user_references.sql - 影響表:
account/session/passkey(userIdFK) Related markers: 無 tasks.md marker(直接修進 migration 0012;本 entry 為 done 狀態的歷史紀錄)
Problem
Migration 0007 / 0009 用 SQLite 的 table-rebuild 慣用法:建 *_new 子表寫成 userId TEXT NOT NULL REFERENCES user_new(id),再對 user_new 做 ALTER TABLE user_new RENAME TO "user",預期 SQLite 會在 RENAME 時把所有子表的 FK 文字從 user_new 自動改寫成 "user"。
這個自動改寫只在 modern SQLite 行為(PRAGMA legacy_alter_table = OFF)才會發生。0007 留了註記說「Verified 2026-04-20 on local D1」、「modern behaviour,default on D1」。
但 NuxtHub local dev 用的是 libsql backend,libsql 預設 legacy_alter_table = 1(legacy 行為,RENAME 不改寫子表 FK 文字)。後果:
sqlite> PRAGMA legacy_alter_table;
1
sqlite> SELECT sql FROM sqlite_master WHERE name IN ('account','session','passkey');
CREATE TABLE "account" (
...
userId TEXT NOT NULL REFERENCES user_new(id) ON DELETE CASCADE,
...
)
-- session、passkey 同樣殘留 user_new ref任何對 account / session / passkey 的 INSERT 都炸:
$ sqlite3 .data/db/sqlite.db "INSERT INTO account ... VALUES (...)"
Error: in prepare, no such table: main.user_newbetter-auth 在 OAuth callback 拼裝 account row 時拋出例外,路徑:@better-auth/oauth2/link-account.mjs 的 c.context.internalAdapter.linkAccount(...) → catch → return { error: "unable to link account" } → /api/auth/error?error=unable_to_link_account。
對使用者影響:fresh local DB 跑 Google OAuth 登入直接被擋,無法建立 session。
Fix approach
新增 0012_fk_rebuild_user_references.sql,對三張表做 explicit-FK rebuild:
CREATE TABLE *_v12 (..., userId ... REFERENCES "user"(id) ...)— 直接寫"user",不依賴 RENAME-rewriteINSERT INTO *_v12 SELECT ...從舊表搬資料DROP TABLE old; ALTER TABLE *_v12 RENAME TO old- recreate 索引(
account_userId_idx、passkey_credentialID_idx、passkey_userId_idx) - 開頭設
PRAGMA legacy_alter_table = OFF防禦未來新表 RENAME
對 production D1(FK 文字已是 "user")→ 此 migration 是慢 no-op;對 local libsql(FK 殘留 user_new)→ 修復成正確 ref。結果上 idempotent。
Acceptance
- [x]
sqlite_master中無任何含user_new的 schema text(SELECT name FROM sqlite_master WHERE sql LIKE '%user_new%';為空) - [x]
INSERT INTO account (id, userId, ...) VALUES ('test', '<existing-user-id>', ...)不再報no such table: main.user_new - [x]
PRAGMA foreign_key_check;乾淨 - [x]
account_userId_idx、passkey_credentialID_idx、passkey_userId_idx三個索引已 recreate - [ ] 重啟
pnpm dev後走 Google OAuth 登入,better-auth 不再回unable_to_link_account(待使用者驗證) - [ ] Production D1 fk_check 仍乾淨、無資料漂移(待 deploy 後驗證;預期為 no-op)
TD-052 — passkey-first-link-google.spec.ts 對 syncUserProfile migrate transaction 缺 stub 鏈
Status: done Resolved: 2026-04-25 — fix-user-profile-id-drift claim 接手後修正:(1) schemaFake 補 4 張 child table descriptor (conversations / queryLogs / messages / documents);(2) select.from(table) 改為 table-aware,mocks.hubDbSelect(table) 依 user / user_profiles 路由不同 state;(3) hubDbSelect 對 user_profiles 查 state.profiles 避免誤入 migrate path;(4) mocks.hubDbUpdate 補 user_profiles 表 update 處理寫回 state.profiles。整套 pnpm test:integration 全綠(81 files / 435 tests / 1 skipped)。 Priority: low Discovered: 2026-04-25 — wire-do-tool-dispatch §5.x SSE Tests 跑 pnpm test:integration 時 cross-spec failure(fix-user-profile-id-drift change 已 wire syncUserProfile 進 hook,但既有 passkey-first-link-google.spec.ts 的 hubDb.transaction stub 不覆蓋 migrate path 的 tx.update(schema.conversations) 呼叫) Location: test/integration/passkey-first-link-google.spec.ts:116(Object.transaction stub)+ server/utils/user-profile-sync.ts:73(migrate transaction 第一個 update) Related markers: search @followup[TD-052] in repo
Problem
fix-user-profile-id-drift change 把 session.create.before hook 改成呼叫 syncUserProfile,內含 db.transaction(async (tx) => { tx.update(schema.conversations).set(...).where(...) ... }) migrate path(當 stale user_profiles.email_normalized 撞到不同 id 時觸發)。
passkey-first-link-google.spec.ts 的 hubDb mock 在 transaction callback 內沒提供 tx.update(...) 完整鏈式 stub — 第一個 .update(schema.conversations) 就拋 TypeError: Cannot read properties of undefined (reading 'userProfileId')。
實質效果:allowlist reconciliation:綁定 allowlist email 後,下次 session refresh 升 admin 並寫 audit 此 test 在跑 runSessionRefresh 時誤觸 migrate path(mock fixture 的 setup 與 stale-row 情境意外重疊,非測試設計)。
不阻擋以下事項:
wire-do-tool-dispatch自身 archive(TD-052 與該 change 無關)fix-user-profile-id-drift自身的 8 個 unit test 在auth-user-profiles-sync.spec.ts仍全綠
阻擋:
pnpm check全套 → integration test 1 failed- 邏輯上應由
fix-user-profile-id-driftarchive 前收尾(該 change 引入 syncUserProfile,但人工檢查或 spec 沒覆蓋既有 spec 的 mock collateral)
Fix approach
兩種選擇:
- Mock 層:擴
passkey-first-link-google.spec.ts的hubDb.transactionstub — 在 transaction callback 內提供完整tx.update().set().where()chain,回傳 mock query builder(最快、scope 最小) - Setup 層:讓
passkey-first-link-google.spec.ts的 user fixture 在email_normalized上不撞 stale row — 確保syncUserProfile走 (id 相同 → UPDATE non-id 欄位) 路徑、不進 migrate transaction(更貼近實際運行情境,但需要對setupAuthDatabasefixture 對齊)
建議走 1:mock 層改動侷限在單一 spec、不影響其他 fix-user-profile-id-drift 的 unit tests。
Acceptance
- [x]
pnpm test:integration test/integration/passkey-first-link-google.spec.ts全綠 - [x] 既有
auth-user-profiles-sync.spec.ts8 個 spec 仍綠(不退步) - [x] 由
fix-user-profile-id-driftchange 收尾(task §8.4 +@followup[TD-052]marker;2026-04-25 done)
TD-053 — fix-user-profile-id-drift production observation(archive 後 follow-up)
Status: done Resolved: 2026-04-26 — v0.50.1 production 立即驗收(取代原 1 週觀察期):過去 7 天 better-auth session.createdAt = 0 條(無新 sign-in 流量)→ session.create.before hook 未被觸發 → 無 user_profiles sync failed 訊號可採。Live wrangler tail 5 分鐘採樣(user 已登入發 6 條 chat,期間 /api/auth/get-session × 2 + /api/conversations × 多次 + POST /api/chat × 6)也未出現任何 user_profiles sync failed log。Hook catch path 在無流量期間穩定,fix-user-profile-id-drift 部署後無 regression 訊號。附帶發現:production 有 6 條 orphaned user_profiles(profile_count=23 vs user_count=17,profile.id 不在 user.id),屬 v0.49.0 / fix-user-profile-id-drift 前的歷史殘留(schema 中 user_profiles.id 並非 user.id 的 FK,無 cascade 清理機制);不阻擋 TD-053 標 done,獨立追蹤於 TD-058。 Priority: low Discovered: 2026-04-25 — fix-user-profile-id-drift archive 時 §9.3 條目轉為 archive 後 follow-up Location: production wrangler tail / Cloudflare logs Related markers: search @followup[TD-053] in repo
Problem
fix-user-profile-id-drift 在 local dev 已驗證 hook INSERT + migrate 兩個 branch 行為正確(archive tasks §7.1 / §7.2 + 8 unit tests)。production 部署後須立即採樣 wrangler tail 確認 hook catch 路徑沒被預期外場景觸發(觸發代表有 stale row 情境在 production 真實出現,須讀 hint 進一步分析)。
Fix approach
部署 fix-user-profile-id-drift 至 production 後立即採樣(取代原「1 週觀察期」):
wrangler tail --env production --format json | jq 'select(.message[]? | contains("user_profiles sync failed"))'(或在 evlog drain 過濾此 wide event 字面值)採樣 5-10 分鐘 + 撈 evlog drain 最近 24h- 計數出現次數
- 若 > 0:讀每筆的
hint欄位(已包含 redacted email + 固定 stale-row 提示),判斷是否為預期 corner case(手動刪 user 重建)或新 regression - 若 corner case:紀錄 → close。若 regression:開新 change 補強 syncUserProfile 邏輯
Acceptance
- [x] Deploy fix-user-profile-id-drift 至 production(含於 v0.49.0 release commit
9805e2f) - [x] 立即採樣 — 2026-04-26 v0.50.1 deploy 後撈 production session 表:過去 7 天
session.createdAt= 0 條,hook 未被觸發;無流量等於無 regression 訊號。Live wrangler tail 採樣為佐證(user 自跑),結果若有觸發再加註此 entry - [x] Status 標
done(無 regression);附帶發現 6 條 orphaned profile 開 TD-058 獨立追蹤
TD-054 — add-new-conversation-entry-points Safari private mode 實機驗證
Status: open Priority: low Discovered: 2026-04-25 — add-new-conversation-entry-points archive 時 §7.6 使用者授權 skip Location: app/utils/chat-conversation-state.ts clearConversationSessionStorage (try/catch QuotaExceededError) Related markers: search @followup[TD-054] in repo
Problem
add-new-conversation-entry-points archive 時,§7.6「Safari private mode 點任一新對話按鈕 → 仍能進新對話畫面、無 toast、無 console error」未本機 Safari 實測。clearConversationSessionStorage helper 已內建 try/catch 涵蓋 QuotaExceededError / DOM Storage disabled 等 edge case,理論上安全,但缺實機評估。
Fix approach
打開 Safari → 開啟「私密瀏覽」視窗 → 進入 production / staging 對話頁面 → 走過三處新對話入口(chat header、sidebar expanded header、sidebar collapsed plus)。確認:
- 點按鈕後成功進入新對話畫面(messages 清空、active state reset)
- 無 toast notification 跳出
- DevTools console 無 error log(QuotaExceededError 應被 helper try/catch 吞掉)
Acceptance
- [ ] Safari private window 三入口各跑一次成功
- [ ] DevTools console 無 error
- [ ] Status 標
done
TD-055 — TD-051 漏網之魚:3 張表的 FK 殘留 _new ref(mcp_tokens_new / query_logs_new)
Status: done Resolved: 2026-04-26 — fix-fk-rebuild-query-logs-chain change 落地:migration 0015(server/database/migrations/0015_fk_rebuild_query_logs_chain.sql)仿 0012 explicit-FK rebuild pattern 重建 query_logs / messages / citation_records 三張表,FK 文字改為 canonical mcp_tokens(id) / query_logs(id)。Spec 補一條 ADDED Requirement「Live DDL Foreign Key References Match Canonical Table Names」進 auth-storage-consistency,把「stored DDL 不得殘留 *_new 字樣」變可測試契約(SELECT name FROM sqlite_master WHERE sql LIKE '%REFERENCES %_new(%' 必須回 0 列)。Synth-broken 端對端驗證:bug 重現(FK on INSERT 炸 no such table: main.mcp_tokens_new)→ 套 0015 → 修復 → INSERT 成功 + foreign_key_check 乾淨 + row counts 保留 + 五個 named index 全在。Production verify (2026-04-26 v0.50.1 deploy 後):wrangler d1 execute agentic-rag-db --remote 跑 stored DDL 檢查回 0 列 + PRAGMA foreign_key_check 回 0 列,與預期 no-op 一致(archive task 2.8 / 4.5 補驗)。 Priority: high Discovered: 2026-04-26 — add-sse-resilience §7.1 local heartbeat 驗證時連環炸:
POST /api/chat在createQueryLog階段 throwSQLITE_ERROR: no such table: main.mcp_tokens_new- 修第一個 FK 後重試,又炸
createMessage:SQLITE_ERROR: no such table: main.query_logs_newLocation:
- 根因:
server/database/migrations/0010_fk_cascade_repair.sql:157+ 同 migration 內messages/citation_recordsrebuild 階段都寫REFERENCES *_new(id),假設後續 RENAME 會自動改寫 - TD-051 修了
account/session/passkey(migration 0012),但漏掉query_logs、messages、citation_records三張 - 三張漏網表:
query_logs.mcp_token_id→mcp_tokens_new(應為mcp_tokens)messages.query_log_id→query_logs_new(應為query_logs)citation_records.query_log_id→query_logs_new(應為query_logs)
Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)
Problem
與 TD-051 完全相同的根因:libsql 預設 legacy_alter_table = 1,ALTER TABLE x_new RENAME TO x 不會自動把其他子表的 FK 文字 x_new 改寫成 x。Migration 0010 的多張 rebuild 仍寫 REFERENCES *_new(id),RENAME 後 FK 文字殘留。
sqlite> .dump | grep "REFERENCES [a-z_]*_new("
mcp_token_id TEXT REFERENCES mcp_tokens_new(id) ON DELETE SET NULL, -- query_logs
query_log_id TEXT REFERENCES query_logs_new(id) ON DELETE SET NULL, -- messages
query_log_id TEXT NOT NULL REFERENCES query_logs_new(id) ON DELETE CASCADE, -- citation_records
sqlite> SELECT name FROM sqlite_master WHERE name IN ('mcp_tokens_new', 'query_logs_new');
(no rows)任何 INSERT 進這三張表都炸 SQLITE_ERROR: no such table,連環擋掉 web chat / MCP 工具呼叫的 query_log + message + citation 寫入路徑。
對 production D1 不影響(D1 的 SQLite 行為是 modern,FK 已正確改寫);對 local libsql fresh DB 全炸。
Fix approach
仿 migration 0012 的 explicit-FK rebuild pattern,新增 0015_fk_rebuild_query_logs_chain.sql(0013 / 0014 已被 messages refused/refusal_reason 擴欄佔用):
- 開頭
PRAGMA legacy_alter_table = OFF+PRAGMA defer_foreign_keys = ON - 對三張表分別 rebuild:
query_logs_v15(FK →mcp_tokens(id))messages_v15(FK →query_logs(id))citation_records_v15(FK →query_logs(id))
- 每張:CREATE → INSERT SELECT → DROP old → RENAME → recreate indexes
- recreate 索引:
idx_query_logs_channel_created_atidx_messages_query_log_id、idx_messages_conversation_created_atidx_citation_records_query_log_id、idx_citation_records_expires_at
- 收尾
PRAGMA foreign_key_check
對 production D1(FK 文字已是正確名稱)→ 慢 no-op;對 local libsql(FK 殘留 _new)→ 修復成正確 ref。結果上 idempotent。
順帶複檢全 schema:sqlite3 .data/db/sqlite.db ".dump" | grep -E "REFERENCES [a-zA-Z_]+_new\(" 應為空。本 entry discovery 時用此檢查確認只剩這三張。
Acceptance
- [ ]
.dump | grep "REFERENCES [a-z_]*_new("為空 - [ ]
INSERT INTO query_logs / messages / citation_records三張都不再報錯 - [ ]
PRAGMA foreign_key_check;乾淨 - [ ] 五個索引(查上方列表)已 recreate
- [ ] Production D1 fk_check 仍乾淨、無資料漂移(待 deploy 後驗證;預期為 no-op)
TD-056 — Workers AI judge 模型 max_completion_tokens: 200 上限被截斷 → JSON parse 失敗 → pipeline_error
Status: open Priority: low high(2026-04-26 重評:TD-061 證明這條是 production 28.6% pipeline_error 的 root cause,不是低頻 cosmetic) Discovered: 2026-04-26 — persist-refusal-and-label-new-chat v0.50.0 production 7.2 verify 抽查 query_logs 時意外撈到。Judge 模型回 JSON 結構包,但 workers_ai_runs_json 顯示 completionTokens: 200 等於上限,content 在 JSON object 中段被截斷,後續 JSON.parse throw → pipeline_error。 Same root cause as: TD-061(acceptance fixture 35 筆 10/10 pipeline_error 全部 completionTokens: 200,見 local/reports/notes/td-061-pipeline-error-investigation-20260426.md) Location:
- judge prompt / model call 位置:
server/utils/workers-ai.ts、server/utils/judge.ts(或對應 retrieve-then-judge orchestrator;apply 時再精準定位) - 截斷觸發 query 樣本:production
query_logs.workers_ai_runs_json帶completionTokens: 200(精確 query id 待 apply 時撈出)
Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)
Problem
Workers AI 對 judge 模型呼叫設定 max_completion_tokens: 200,正常 judge JSON 多數在 ~150 tokens 內可結束,但少數 query(候選文件多 / reasoning 較長)會撞 200 token 上限:模型輸出在 JSON 中段被硬截 → 字串非合法 JSON → JSON.parse(...) throw → pipeline 走 refusal 路徑回 pipeline_error。
UX 影響有限:v0.50.0 已落地 persist-refusal,使用者會看到 refusal message + reason pipeline_error,不再「整個訊息消失」;但實際上是模型本來能回答、只是被 token 上限切斷。等於把可救援的查詢誤標 refused。
Fix approach
兩條路擇一(apply 時用實機資料 sampling 決定):
- 抬高
max_completion_tokens(例如 400 或 512)— 最低成本,但長尾 query 仍可能再撞牆 - Judge 模型輸出 schema 改 schema-constrained / structured output(若 Workers AI 該模型支援 JSON mode / response_format)— 強制模型在預算內結束 JSON,超過則 partial 而非 mid-string truncation;搭配 fallback parser 容忍尾段不完整
兩條都要:
- 加 evlog 在
completionTokens達 max 時記 wide event field(ex:judge.token_truncated: true),便於後續 production sampling - pipeline_error 時保留 raw response snippet(前 N 字元)到
query_logs,方便事後 audit
Acceptance
- [ ] Fix 部署後立即撈 production query_logs 連續 24-48h 樣本,驗
judge.token_truncated: true或pipeline_error觸發次數收斂到 0(或對照 baseline 顯著下降)—— 取代原「過 7 天觀察」 - [ ] Local repro:把 judge max_completion 故意調 50 重現截斷 → 修復後 fix path 觸發、不 throw
- [ ] judge 模型呼叫具備 instrumentation,可區分「真正 refusal」vs「token-budget 截斷」
TD-057 — evlog wide event lifecycle 警告:log.error() / log.set() 在 wide event emit 後呼叫,SSE stream 真實錯誤與結果欄位被吞
Status: open Priority: mid Discovered: 2026-04-26 — production wrangler tail 重複出現兩種同 root cause 的 warning:
[evlog] log.error() called after the wide event was emitted — Keys dropped: operation, error.
[evlog] log.set() called after the wide event was emitted — Keys dropped: result.Production live tail 採樣(2026-04-26 21:38-21:40 UTC,6 條 chat):
| Warning 類型 | 次數 | 對應 chat 類型 |
|---|---|---|
log.error() keys dropped: operation, error | 3 | 全部對應 D1 refusal_reason='pipeline_error' 的 chat |
log.set() keys dropped: result | 3 | 對應 2 條 no_citation refusal + 1 條成功 chat(first_token=3245ms) |
兩種 warning 同 root cause:wide event 在 handler return 時已 emit,SSE ReadableStream.start() 內後續 log.set() / log.error() 都 race 失敗。
Location:
server/api/chat.post.tscreateSseChatResponse的ReadableStream.start()callback- 上層 wide event 在 SSE response 建立時 emit 完畢;
start()內 stream pipe 若 throw,catchblock 試圖log.error(err, { operation, error })已無 owning event,evlog 丟掉 keys
Related markers: 無 tasks.md marker(本 entry 為發現紀錄)
Problem
evlog 設計上一個 request 對應一個 wide event。createSseChatResponse 把 SSE stream 建立後立即 return Response,wide event 在 handler 結束時 emit。SSE 內部的 ReadableStream.start() callback 是異步、長壽命:當 stream 中段 throw(例如 Workers AI fetch 中斷、token decode 失敗),catch handler 呼叫 log.error(err, ...) 時 wide event 已 emit,evlog 紀錄 warn + drop keys。
實際後果:production 看到 pipeline_error 但 wide event 缺 operation / error 細節,無法追真實錯誤;只能靠 wrangler tail 撈當下 console,過了就找不回。直接影響 SSE 路徑可觀察性。
Fix approach
兩條路擇一(apply 時定):
log.fork('sse-stream', fn)模式 — evlog 若支援 sub-event:在ReadableStream.start()內 fork 一個新的 wide event lifecycle,error 寫進 sub-event 而非 parent- 延後 wide event emit — 把 wide event lifecycle 改成跨 stream(handler return 前不 emit,等 stream 自然結束或 abort 才 emit);技術上需 evlog 支援「stream-aware」生命週期,或自寫 wrapper
兩條都需要:
- 整理當前
createSseChatResponse的 wide event lifecycle 圖(哪個 emit、何時 emit)寫進 design / docs - 確認其他 SSE / streaming endpoint 是否同樣 pattern(avoid one-off fix)
Acceptance
- [ ] Production wrangler tail 不再出現
[evlog] log.error() / log.set() called after the wide event was emittedwarning - [ ] SSE stream 中段 error 可在 wide event / sub-event 中看到
operation+errorkeys - [ ] SSE 成功路徑的
resultfield 也能保留(不再被 lifecycle drop) - [ ] 其他 streaming endpoint 不再有相同 lifecycle 漏洞
TD-058 — Production user_profiles 6 條 orphaned rows(profile.id 不在 user.id)
Status: open Priority: low Discovered: 2026-04-26 — TD-053 production 立即驗收時撈表發現 profile_count=23 vs user_count=17Location: production D1 user_profiles 表 Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)
Problem
Production user_profiles 表有 6 條 orphaned row:SELECT COUNT(*) FROM user_profiles WHERE id NOT IN (SELECT id FROM user) 回 6。原因:user_profiles.id schema 為 TEXT PRIMARY KEY 而非 REFERENCES user(id),無 FK + 無 cascade,因此 better-auth 自刪 user 時 user_profiles row 不會自動清理;此外 fix-user-profile-id-drift 之前的舊版 session.create.before hook 也可能在 stale row 情境下產生 id 漂移殘留。
非 functional bug(user_profiles 在無對應 user 時不會被任何 hot path 讀到),但會影響:
- 報表 / 統計類 query 數字膨脹
- UNIQUE
email_normalized假設未來重新註冊同 email 時可能踩到既有 orphaned row
Fix approach
兩階段:
- 觀察與分類:撈這 6 條 orphaned row 的
email_normalized/created_at/role_snapshot,比對是否對應 better-authaccount.providerId='google'中已自刪的測試帳號或更早的 schema migration 殘留。判斷是否安全清除。 - 清理:寫 one-shot migration 或 SQL script 刪這 6 條(或保留有業務意義者)。一併評估是否將
user_profiles.id改為REFERENCES user(id) ON DELETE CASCADE(需謹慎,因為 better-auth 自刪 user 時的順序敏感)。
Acceptance
- [ ] 6 條 orphaned row 已分類(業務 vs 殘留)並文件化於本 entry
- [ ] 清理腳本或 migration 落地,production
profile_count == user_count(除預期保留) - [ ] 評估
user_profiles.id加 FK 的可行性與 ON DELETE 行為,決策結論寫在此 entry 或獨立 ADR
TD-059 — E2E Tests CI workflow 連續 50+ run 全紅(webServer ESM scheme 'cloudflare:' 不支援)
Status: done Priority: high Discovered: 2026-04-26 — v0.50.1 ship 後驗收時 gh run list --workflow="E2E Tests" 抽 50 條全 failure(最早 2026-04-24 15:52 起,跨 50+ commit、含 v0.49.0 / v0.50.0 / v0.50.1 三次 release) Resolved: 2026-04-26 — 採方案 A(wrangler dev);待 CI run 驗證 3 條綠燈後可關閉 Location:
- CI workflow
.github/workflows/*.yml(E2E Tests job) playwright.config.tswebServer設定(啟動nuxt preview).output/server/build artifact 含cloudflare:protocol import(typical:cloudflare:workersruntime built-ins)
Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)
Problem
E2E CI workflow 跑 pnpm test:e2e → Playwright webServer 啟動 nuxt preview 在 Node 22 環境,Node 預設 ESM loader 解析 .output/server/ 內 import 'cloudflare:workers'(或類似 cloudflare:* scheme)時 throw:
Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'cloudflare:'
at throwIfUnsupportedURLScheme (node:internal/modules/esm/load:187:11)webServer process exit 1,Playwright 還沒跑任何 spec 就 fail。E2E 已連紅 1 天以上、50+ commit、3 次 release,等於 ship gate 完全失能 — 任何 e2e regression 都不會被攔截。
Root cause
CI 設 NITRO_PRESET=node-server env var 試圖切換到 Node preset,但 nuxt.config.ts:339 硬編 nitro.preset: 'cloudflare_module' 會勝過 env var → build 出 cloudflare-module artifact(每個 chunk 都 import "cloudflare:workers")。nuxt preview 在 CI 找不到 .output/server/wrangler.json → 回退用 Node 直接執行 → 撞 ESM scheme 不支援。
Resolution(採方案 A)
playwright.config.ts:CI 分支從 nuxt preview 換成
bash
pnpm exec wrangler dev --config .output/server/wrangler.json --port 3010 --ip 127.0.0.1 --persist-to .wrangler/e2e-state --log-level warn並把 webServer.timeout 拉長到 120s(wrangler dev 首次啟動會 download workerd binary)。
.github/workflows/e2e.yml:
- 移除
NITRO_PRESET=node-server(no-op)、NUXT_HUB_LIBSQL_URL、Patch NuxtHub local DB driver、Bootstrap local sqlite schema(libsql 時代殘留) - Build 加
NUXT_KNOWLEDGE_MCP_CONNECTOR_CLIENTS_JSON(baked into runtime config snapshot) - 新增
Generate .dev.vars for wrangler devstep(runtime 從.output/server/.dev.vars讀 worker env vars) - 新增
Bootstrap local D1 schema via wranglerstep:跑 15 條 migration 用wrangler d1 execute DB --local --persist-to .wrangler/e2e-state Run E2E testsstep 不再帶 env block(runtime config 改由.dev.vars提供)
本機驗證:pnpm exec wrangler dev 2 秒內啟動、HTTP 200、D1 binding 正常。
Trade-offs
- +對齊 production runtime:workerd 而非 Node,binding(D1 / KV / R2 / AI / Durable Objects)走真實 Cloudflare 路徑
- +移除 libsql 兩條 patch link:不再需要
patch-hub-db-dev.mjs的 CI 鉤、NUXT_HUB_LIBSQL_URLenv 注入 - −CI step 數略增(多
Generate .dev.vars、Bootstrap D1兩步),但每步 <30s - −首次 wrangler dev 啟動 download workerd binary(GitHub runner 上估計 ~5–10s 一次性成本)
Acceptance
- [x] 本機
pnpm exec wrangler dev啟動成功並回 200 - [x] D1 schema bootstrap 全 15 條 migration 跑過(local smoke test)
- [ ] CI E2E workflow 至少連續 3 條 run 通過(含 main push + tag push)— 待 commit + push 後驗證
- [x] webServer 啟動 log 不再出現
ERR_UNSUPPORTED_ESM_URL_SCHEME(local smoke 已確認) - [x] Fix 的 trade-off(runtime 對齊 / startup 時間 / DB binding 處理)已記錄在本 entry
TD-060 — Production agentic-rag AutoRAG 對 seed acceptance fixture 的 retrieval_score 全低於 directAnswer 門檻
Status: in-progress(實作於 change rag-query-rewriting,staging deploy 後驗收 acceptance evidence) Priority: high Discovered: 2026-04-26 — main-v0.0.54-acceptance run 對 production https://agentic.yudefine.com.tw/mcp 跑 35 筆 seed cases,D1 query_logs 顯示 retrieval_score 平均 0.32–0.44,全部 35 筆都低於 thresholds.directAnswerMin=0.7Location: production agentic-rag AutoRAG index、shared/schemas/knowledge-runtime thresholds、server/utils/knowledge-* retrieval pipeline Related markers: search @followup[TD-060] in repo
Problem
main-v0.0.54-acceptance-latency-run-20260426.md 對 production 跑 35 筆 seed acceptance fixture,query_logs.retrieval_score 統計:
- mean ~0.36
- range 0.28–0.44
- 全部 35 筆 <
thresholds.directAnswerMin=0.7 - 全部 35 筆 <
thresholds.judgeMin=0.45
結果:
- 23 筆
decision_path=no_citation_refuse(治理層保守拒答) - 10 筆
decision_path=pipeline_error(重測批次,見 TD-061) - 2 筆
decision_path=restricted_blocked(TC-13 / r2,正確 scope 阻擋)
0 筆進入 direct / judge_pass / self_corrected 路徑。
影響:
main-v0.0.54-acceptance報告無法量化「實模型回答品質」(Judge 觸發率、引用正確率、回答正確率)- 真實使用者若拿類似中文口語 prompt 問
askKnowledge,也會 100% 走治理拒答路徑 - 治理保險機制本身運作正確(不幻覺),但 RAG 實際可用性 = 0%
可能原因:
- AutoRAG 索引 ingest 的文件主題跟 seed prompts 不對應(seed 是 ERP / SOP 主題)
- AutoRAG embedding model(
@cf/qwen/qwen3-embedding-0.6b)對中文口語匹配能力不足 - Chunk 切分策略未讓 score 衝高
thresholds.directAnswerMin=0.7對 AutoRAG score 分布來說設過高
Fix approach
- 檢查 production
agentic-rag索引內容(CF Dashboard 看 ingested 文件主題、筆數、最後 sync) - 抓最近一週 production
query_logs全量retrieval_score分布,看 P95;若 P95 都低於 0.7,調整directAnswerMin到實際分布合理值 - 跑 AutoRAG re-index 用更精細 chunk size(256 tokens)
- 若 1-3 都沒救:考慮換 embedding model 或補 reranker
Acceptance
- [ ] Production
query_logs連續一週新樣本中至少 30%decision_path != no_citation_refuse - [ ] 重跑
main-v0.0.54-acceptance33 筆 fixture,至少 50% 拿到 direct / judge_pass / self_corrected - [ ] thresholds 校正決策寫入
docs/decisions/YYYY-MM-DD-rag-thresholds.md
TD-061 — Production query_logs 重測批次 pipeline_error 28.6%(10/35)
Status: open Priority: high Discovered: 2026-04-26 — main-v0.0.54-acceptance run 對 production 跑 50 筆 fixture(33 unique + 17 重測),D1 query_logs.decision_path=pipeline_error 出現 10 次,全部集中在 r2 重測批次 Location: server/utils/knowledge-*、server/api/mcp/**、production rate-limit、AutoRAG pipeline Related markers: search @followup[TD-061] in repo
Acceptance dependency:本 TD 的最終 acceptance 驗證依賴 change
rag-query-rewritingramp staging — 必須先讓 fixture 能進 judge gate(即 retrieval_score ≥0.45),才有真實 judge truncation 路徑可 verify TD-061 的 fix 是否消除 pipeline_error。
Problem
對 production 跑 50 筆 fixture:
- 第 1–35 筆(33 unique seed + 前 2 筆 r2 重測)皆正常返回(
accepted/blocked) - 第 36–50 筆全部觸發 HTTP 429,未進 D1
- D1 已寫入的 35 筆裡,
pipeline_error10 筆樣本特徵:completion_latency_ms = NULL、retrieval_score = NULL、judge_score = NULL、refusal_reason = pipeline_error
10 筆對應的 prompt 包括 TC-05 / TC-06×2 / TC-12 / TC-13 / TC-14 / TC-18×2 / TC-20 / EV-01。
2026-04-26 D1 raw evidence 調查(詳見 local/reports/notes/td-061-pipeline-error-investigation-20260426.md):
- 10/10 pipeline_error row 的
workers_ai_runs_json都記錄了唯一一筆agentJudge(@cf/moonshotai/kimi-k2.5),且completionTokens全部剛好等於 200(max ceiling) - 23 筆
no_citation_refuse全部retrieval_score ∈ [0.28, 0.44](<judgeMin=0.45,judge 沒被觸發) - 「r2 批次」框架其實是 score-banding 假象 — 真實切割線是 retrieval 是否落入 [judgeMin=0.45, directAnswerMin=0.7) → 進 judge → 100% 觸發 bug
可能原因(按證據強度排序):
- HIGH (~85%):
server/utils/workers-ai.ts:135agentJudgemax_completion_tokens: 200對於需要reformulatedQuery的 case 不夠 → 截斷 JSON →normalizeStructuredResponse(line 416-418) 直接呼叫JSON.parse對截斷字串拋SyntaxError→ web-chat.ts:426 catch block 寫pipeline_error+ 把 retrievalScore 寫為 NULL(line 447) - ~10%:Workers AI runtime 對
response_format: json_schema的 grammar constraint 支援不完整,回傳非結構化字串 - ~5%:cache key collision / pipeline 不冪等(證據不支持:每筆有獨立 judge latency,無共用 run 跡象)
影響:
- 真實使用者短時間重複問同 prompt 約每 4 個請求就 1 個拿到 pipeline_error
- 治理層保險仍正常(不幻覺、messages.content_text 不寫原文),但使用者體驗顯示為「服務無回應 / 不穩定」
Fix approach
- 驗證假設 1:vitest unit test mock
readJudgeResponse對截斷 JSON 字串(如'{"shouldA')的行為,確認拋SyntaxError - 修法(兩條軸線並行):
- 軸線 A:提高 agentJudge
max_completion_tokens至 512–1024(reformulatedQuery 中文重述 200 token 顯然不夠) - 軸線 B:
readJudgeResponse加 truncation guard — 檢查response.usage.completion_tokens是否觸頂;若是則明確拋JudgeTruncationError,catch block 寫decision_path=judge_truncated - 軸線 C:
normalizeStructuredResponse加 try/catch + jsonrepair fallback,部分結構可用就用
- 軸線 A:提高 agentJudge
- 拆
pipeline_errorenum:至少拆出judge_error/judge_truncated/retrieval_error/composer_error,schema migration - acceptance fixture 補 retrieval ∈ [0.45, 0.7] 的 case:本批 35 筆都跑不到 judge 區間後又能 succeed,意味 judge 路徑長期沒有 production 監控
- 與 TD-057 的關係:TD-057 修不會自動解 TD-061(TD-057 是觀測層警告、TD-061 是功能層 bug,兩者獨立);但 TD-057 修完後 wide event 會帶完整 stack,可作為 TD-061 fix verification 的驗證信號
Acceptance
- [ ] 找出 10 筆 pipeline_error 的根因類別(至少分 2 類)
- [ ] 連續 50 筆同 prompt 重測,pipeline_error rate < 5%
- [ ]
decision_pathenum 擴張到分類錯誤(schema migration) - [ ] Production 連續一週 pipeline_error rate baseline 寫入
docs/verify/
TD-062 — Extract buildRetrieveWithRewriter helper across 3 entry points
Status: open Priority: mid Discovered: 2026-04-26 — /commit 0-A simplify review on change rag-query-rewritingLocation: server/api/chat.post.ts:192-217、server/mcp/tools/ask.ts:128-154、server/mcp/tools/search.ts:79-106Related markers: search @followup[TD-062] in repo
Problem
三個 entry point 都建立同一形狀的 retrieve closure:判斷 input.useRewriter !== false && isQueryRewritingEnabled(runtimeConfig),組裝同樣 shape 的 rewriteForRetrieval 參數,呼叫 retrieveVerifiedEvidence。約 28 LoC × 3 = 重複面積大;未來要改 rewriter 接口要動三個檔。
Fix approach
抽 buildRetrieveWithRewriter({ runtimeConfig, event, search, store, governance, onRewriterOutcome? }) helper,回傳 (input) => Promise<...> closure。chat.post.ts / mcp/tools/ask.ts 透過 onRewriterOutcome callback 取得 last status / rewrittenQuery 寫進 audit;mcp/tools/search.ts 不需要。helper 放在 server/utils/knowledge-query-rewriter.ts 或 server/utils/knowledge-retrieval.ts。
Acceptance
- 三個 entry point 各自從 ~28 LoC closure 縮成 ~5 LoC(call helper + optional callback)
- 既有 unit + integration test 全綠不需改
pnpm typecheck通過
TD-063 — Trim duplicated useRewriter callback docstring
Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A simplify review on change rag-query-rewritingLocation: server/utils/knowledge-answering.ts:65-73、server/utils/web-chat.ts:186-193、server/utils/mcp-ask.ts:171-180、server/utils/mcp-search.ts:14-22Related markers: search @followup[TD-063] in repo
Problem
四處 retrieve callback signature 都各自寫 6-9 行 docstring,重述「retry pass 必須 useRewriter: false 因為 reformulatedQuery 已是 LLM-shaped query」同一段話。違反專案規則「Default to writing no comments」「Don't reference the current task / fix / callers」。
Fix approach
把完整 rationale 留在 server/utils/knowledge-query-rewriter.ts 開頭一份 canonical docstring;四個 callsite 縮成單行 // see knowledge-query-rewriter.ts §S-RW for retry-pass rationale。
Acceptance
- 四個 callsite docstring ≤ 1 行
- canonical docstring 仍涵蓋 retry pass rationale
pnpm typecheck通過
TD-064 — Integration test mocks DB; should be relocated or replaced with real D1
Status: open Priority: mid Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: test/integration/retrieve-verified-evidence-with-rewriter.spec.ts、server/utils/knowledge-audit.ts:359-400Related markers: search @followup[TD-064] in repo
Problem
test/integration/retrieve-verified-evidence-with-rewriter.spec.ts 用 vi.fn() mock 了 search 與 resolveCurrentEvidence,沒有真實 D1 / evidence store / auditStore.updateQueryLog round-trip。違反 .claude/rules/testing-anti-patterns.md 「no mocking DB in integration tests」。更關鍵:本 change 風險最高的程式碼之一—— knowledge-audit.ts 的 dynamic SET clause UPDATE—— 沒有任何 test 覆蓋 bind ordering / SQL composition。未來開發者改 setClauses 順序會悄悄壞 audit 寫入。
Fix approach
兩擇一:
- (a) 把這個 spec 移到
test/unit/,並為 audit dynamic UPDATE 另外寫一個 D1-backed integration test(in-memorybetter-sqlite3或 D1 local),assertrewriter_status/rewritten_query經完整 INSERT → UPDATE → SELECT round-trip 結果正確 - (b) 保留檔名位置但改寫內容用真實 D1,覆蓋 audit dynamic UPDATE 的 setClauses 順序
Acceptance
- 至少一個 integration-tier test 對 audit dynamic UPDATE 用真實 D1 round-trip
- 測試 cover:rewriter_status / rewritten_query 都帶 / 都不帶 / 一個帶一個不帶 三種組合
pnpm test全綠
TD-065 — UpdateQueryLog.rewriterStatus 型別與 NOT NULL 欄位不一致
Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: server/utils/knowledge-audit.ts:357-388Related markers: search @followup[TD-065] in repo
Problem
UpdateQueryLog.rewriterStatus 型別宣告 string | null,但 query_logs.rewriter_status schema 是 TEXT NOT NULL DEFAULT 'disabled'。caller 傳 null 會 bind SQL NULL 進 NOT NULL 欄位 → D1 constraint error 包成 5xx。目前 caller 都從 lastRewriterStatus: string = 'disabled' 起手,永遠不傳 null,是 latent bug。
Fix approach
UpdateQueryLog.rewriterStatus?: string(drop| null)- 確認
bindings.push(input.rewriterStatus ?? null)改成bindings.push(input.rewriterStatus)(undefined 已被setRewriterStatus旗標濾掉) - typecheck 通過
Acceptance
- 型別不再允許
null - typecheck + 既有 test 通過
- 加一條 unit test:傳
'disabled'/'success'/'fallback_timeout'等都 round-trip 正確
TD-066 — RewriterStatus discrimination 缺 assertNever
Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: server/utils/knowledge-retrieval.ts:137-138Related markers: search @followup[TD-066] in repo
Problem
rewriteResult.status === 'success' ? auditKnowledgeText(...).redactedText : null 是單一 equality check,未走專案規定的 switch + assertNever pattern(見 .claude/rules/development.md 與 ux-completeness.md Exhaustiveness Rule)。將來新增 RewriterStatus 值(如 fallback_blocked)時不會 compiler error。
Fix approach
typescript
import { assertNever } from '~/utils/assert-never'
let rewrittenQueryForAudit: string | null
switch (rewriteResult.status) {
case 'success':
rewrittenQueryForAudit = auditKnowledgeText(rewriteResult.rewrittenQuery).redactedText
break
case 'fallback_timeout':
case 'fallback_error':
case 'fallback_parse':
rewrittenQueryForAudit = null
break
default:
assertNever(rewriteResult.status, 'retrieveVerifiedEvidence rewriter')
}Acceptance
- 新增
RewriterStatus值(暫測fallback_blocked)時 typecheck 立刻 fail - 移除測試後既有 4 個 status 全綠
pnpm audit:ux-drift不報新漂移
TD-067 — test/tsconfig.json baseline 191 errors
Status: open Priority: mid Discovered: 2026-05-04 — clade v0.3.10 cutover 把 test typecheck 加到 pre-push 階段時揭露 Location: test/tsconfig.json + 63 個 test files Related markers: search @followup[TD-067] in repo
Problem
pnpm exec tsc -p test/tsconfig.json --noEmit 跑出 191 errors in 63 files,分類:
- Component module not found(最大宗,TS2307)
~/components/chat/MarkdownContent.vue、~/components/chat/ConversationHistory.vue、~/components/chat/RefusalMessage.vue、~/components/auth/DeleteAccountDialog.vue、~~/app/components/admin/usage/TimelineChart.vue、~~/app/components/debug/OutcomeBreakdown.vue等- 真實檔案存在,但 test path resolver 找不到(alias / Nuxt auto-import gap,
.nuxt/types 或 test tsconfig paths 沒涵蓋)
- Mock/fixture type drift(TS2352)
acceptance-auth/bindings/fixtures.test.ts、chat-route-heartbeat.spec.ts等的 fixture 跟ChatConversationMessage型別漂移(後者新增refused、refusalReason欄位)
- Nitro route key inference 撞「excessive stack depth」(TS2321)
create-chat-conversation-history.spec.ts— Nitro 端 type generic 太深
legacy-test-roots.test.ts(6,8)TS5097- 缺
allowImportingTsExtensions: true
- 缺
middleware-admin.test.ts函式簽名漂移- middleware 業務 signature 改成 2 args,test 只給 1
- 47× TS2345、22× TS18048 跨 mcp/integration tests
vitest.config.ts:130,140TS2345- vitest 4.x
TestProjectInlineConfiguration.plugins不接受unknown[],需顯式 cast 或 import 正確 type
- vitest 4.x
test/unit/sse-parser.spec.tsTS2322onBlockcallback 隱式 return widening(Promise<string>vsPromise<'continue' | 'terminate'>),需return 'continue' as const或顯式 type annotation
clade v0.3.10 把 test-tsconfig 放 pre-push 後立即擋下;clade v0.3.11 把該 check 從中央倉移除(因 5 家 consumer 中有 test/tsconfig.json 的 3 家裡 2/3 baseline 紅,hit rate 過高)。目前不擋 push,但 baseline 仍存在,是真實 type safety gap。
Fix approach
按錯誤類型分批:
- Component module not found(先做,最大宗)
- 確認 path alias(
~/、@/、~~/、#shared/)在test/tsconfig.jsonpaths 設定不全,或 Nuxt auto-import 沒對 test scope 生效 - 修
test/tsconfig.jsonpaths 或extends的 base map
- 確認 path alias(
- Mock/fixture type drift:grep
ChatConversationMessagefixture,補上新增欄位 legacy-test-roots.test.ts:加allowImportingTsExtensions: true到 test tsconfigvitest.config.ts:plugins 加as PluginOption[]cast,或 import 正確 typesse-parser.spec.ts:onBlock callback return'continue' as const或顯式 annotation- Nitro route key excessive depth:用
as anyworkaround 或升級 Nitro middleware-admin.test.ts:找 middleware 真實 signature,補第二個 arg- 47× TS2345 / 22× TS18048:per-file 分析(mock signature drift /
Object is possibly undefined補 narrow)
Acceptance
pnpm exec tsc -p test/tsconfig.json --noEmit 0 errors。完成後可向 clade 中央倉爭取「opt-in 重啟 test-tsconfig pre-push check(hub.json flag)」— 但需先驗證至少 2 家 consumer baseline 也綠。
TD-068 — deploy.yml 兩個 wrangler-action step 缺 secrets: list(違反 cf-workers/secrets.md rule)
Status: open Priority: mid Discovered: 2026-05-09 — clade v0.5.25 新增 rules/modules/runtime/cf-workers/secrets.md 後對 5 consumer 跑 verify checklist 揭露 Location: .github/workflows/deploy.yml — 「Deploy to Cloudflare Workers (production)」(line 156-163)+「Deploy to Cloudflare Workers (staging)」(line 254-261) Related markers: search @followup[TD-068] in repo
Problem
兩個 wrangler-action step 只有 apiToken / accountId / command,沒帶 secrets: | block。意味 worker runtime 用到的 secret(BETTER_AUTH_SECRET / NUXT_SESSION_PASSWORD / ADMIN_EMAIL_ALLOWLIST / MCP_CONNECTOR_CLIENTS_JSON 等)目前不會在每次 deploy 時自動 sync — 必須過去某個 session 手動跑 wrangler secret put 推上去,後續 rotation 容易脫節。
build env 那邊雖然有 ${{ secrets.PROD_BETTER_AUTH_SECRET }} / ${{ secrets.STAGING_BETTER_AUTH_SECRET }} 等,但那是 build-time 注入,build artifact 編進去;runtime secret 仍要走 wrangler secret API(wrangler-action secrets: list 或手動 wrangler secret put)。
違反 clade rule rules/modules/runtime/cf-workers/secrets.md:「Single source of truth = GitHub repo secret;MUST 在 deploy yaml 用 wrangler-action secrets: list 推進 worker;NEVER 手動 wrangler secret put」。
Fix approach
- 列出 production worker 跟 staging worker 實際 runtime 需要哪些 secret(
pnpm exec wrangler secret list+ 比對 server code 內process.env.X引用) - 確認 GitHub repo 對應的
PROD_*/STAGING_*Secret 都在 - 改 deploy.yml 兩個 step:
- 加
secrets: |list 列出所有 runtime secret name(worker 端的名稱,不帶前綴) - 加
env:block 對應${{ secrets.PROD_<NAME> }}/${{ secrets.STAGING_<NAME> }}
- 加
- 比對 Notion 是否有 agentic-rag 的 secret 紀錄頁面;無則補
- 跑 staging deploy 驗證
secrets:list 自動 sync
Acceptance
deploy.yml兩個 wrangler-action step 都有secrets: |+env:block- staging deploy run log 看到
Found <N> secrets, uploading...訊息 - production worker / staging worker
wrangler secret list比對 GitHub Secret 內容一致 - Notion「GitHub Secrets & 環境變數」(或 agentic-rag 對應 page)有 secret 列表
為什麼登記不立即修
當下 session(2026-05-09 21:30)user 指示「先登記不處理」— agentic-rag 自家 worker 的 secret 拓樸需要時間 audit(多個 build env / runtime secret / NuxtHub bindings 混合),且觸碰 deploy workflow 風險高,要排獨立 session 處理。
TD-069 — T3 evlog 落地 production 缺 D1 evlog_events migration(drain 在 prod 是 dead-write)
Status: open Priority: high — T3 evlog 在 production 形同無作用,所有 wide event drain 都會 silently fail Discovered: 2026-05-10 — clade HANDOFF §2.4 dev smoke 跑 wrangler d1 execute agentic-rag-db --remote --command "SELECT count(*) FROM evlog_events" 回 no such table: evlog_events: SQLITE_ERROR [code: 7500]Location: server/database/migrations/(缺檔)+ nuxt.config.ts @evlog/nuxthub module wire(已 wire 但未生 migration) Related markers: 對應 spectra change archive/...adopt-evlog-nuxthub-ai-t3/ tasks §7.1-7.4 全部 unchecked
Problem
T3 spec apply(commit dbea28d)後 @evlog/nuxthub module 已正確 wire 進 nuxt.config.ts:96-114,119-124,runtime drain plugin 也會在每筆 wide event 時嘗試 INSERT 進 D1 evlog_events table。但 production D1 從來沒這張 table — 因為 T3 apply 時沒跑 pnpm hub:db:migrations:create 把 @evlog/nuxthub 經 hub:db:schema:extend hook 注入的 schema 落成 drizzle migration。
驗證命令:
bash
cd ~/offline/nuxt-edge-agentic-rag
export CLOUDFLARE_API_TOKEN=$(grep -E "^CLOUDFLARE_API_TOKEN=" .env | head -1 | cut -d= -f2-)
npx wrangler d1 execute agentic-rag-db --remote --command "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%evlog%'"回傳:空(沒任何 evlog* table)。
server/database/migrations/ 16 條 SQL(0001 → 0016)grep evlog_events 0 命中。
機制:@evlog/nuxthub setup() 內 nuxt.hook('hub:db:schema:extend', ({ dialect, paths }) => paths.push(resolve2('./runtime/db/schema/events.${dialect}'))) 只負責註冊 schema 到 nuxthub drizzle pipeline;要實際生成 migration 必跑 pnpm hub:db:migrations:create,不是 deploy 時自動執行。
Impact
T3 evlog adoption 表面 audit clean(drain.pipelineWraps=1 / nuxthub.moduleInstalled=1 / enrichers.installed=5,audit script 0/4 block),但實際 production runtime 每筆 wide event drain 都會 INSERT 進不存在的 table → 失敗 → drain.js retry 3 次 → 全敗 → silently swallowed。production 上 0 筆 evlog wide event 被持久化。
對應 spectra change tasks §7.1(SELECT count > 0)/ §7.2(event.ai.cost_usd 寫入)/ §7.3(SSE child event 入 D1)/ §7.4(event.actor.id 在 D1 row)全部無法驗。
Fix approach
bash
cd ~/offline/nuxt-edge-agentic-rag
pnpm hub:db:migrations:create
# 應該產生 server/database/migrations/0017_<auto-name>.sql 含 CREATE TABLE evlog_events
ls server/database/migrations/
git diff server/database/migrations/ # 檢查 schema 是否符合 @evlog/nuxthub 預期
git add server/database/migrations/
git commit -m "🐛 fix(evlog): missing D1 migration for evlog_events table (TD-069)"
git push origin main # staging deploy 自動套 migration
# 或對 production:tag + deploy production 路徑deploy 完跑:
bash
npx wrangler d1 execute agentic-rag-db --remote --command "SELECT count(*) FROM evlog_events"
# 應回非 0(@evlog/nuxthub drain 開始 INSERT 後)Acceptance
- production D1
evlog_eventstable 存在 SELECT count(*) FROM evlog_events WHERE created_at > now() - interval '1 hour'> 0(chat endpoint 觸發後)event.ai.cost_usd / event.ai.tokens / event.ai.tool_calls在新 row 內可查- spectra change
adopt-evlog-nuxthub-ai-t3/tasks.md §7.1-7.4可勾完成
風險
pnpm hub:db:migrations:create 會根據 drizzle schema diff 生 migration。若 nuxthub events.sqlite schema 跟 production D1 已有 schema 有 indirectly conflict(例如 index name 撞),需 review migration SQL 再 commit。不要直接 migrations:create 後盲 push。
TD-070 — rag-query-rewriting 人工檢查對齊新 manual-review 規範
Status: open
Priority: mid
Discovered: 2026-05-12 — clade v1.3.6 manual-review.md 新增 Pre-Review Data Readiness + [review:ui] 收斂原則 + verify channel marker schema(散播到 consumer LOCKED 投影)
Location: openspec/changes/rag-query-rewriting/tasks.md ## 人工檢查 section(items 1-7)
Related markers: 此 TD 屬「整批 ingest 工作」追蹤,不在 tasks.md 內標 @followup marker
Problem
rag-query-rewriting 的 ## 人工檢查 7 items 未標 marker。按 clade v1.3.6 新規 manual-review.md「Fallback ≠ 允許省略」:所有新寫或 ingest 修改的 items MUST 顯式標 marker。7 items 內容多屬 staging acceptance evidence 抽查(retrieval_score / latency p95 / fallback rate / 抽 query_log_debug 記錄 / production safety check / Decision Q1 / Decision Q2)→ 該全部標 [discuss](Claude 主導 evidence 收集 + user walkthrough 拍板)。
User 決定本次 session 登記不處理(2026-05-12 對話中明示「agentic-rag 全部都登記 不處理」)。
Fix approach
對 7 items 各加 [discuss] marker(schema: - [ ] #N [discuss] <description> @no-screenshot if applicable)。逐項 evidence 在 spectra-archive Step 2.5 Discuss Walkthrough 流程由 Claude 準備、user 拍板 OK 後寫 (claude-discussed: <ISO>) annotation。Items 1-7 對應動作:
- #1-#3:retrieval_score / latency / fallback rate — 對應 task §6.5 acceptance evidence
- #4:抽 3 條 staging
query_log_debug記錄人工判斷改寫合理性 - #5:production safety check(features.queryRewriting=false)
- #6-#7:Decision Q1/Q2 商業判斷
Acceptance
- 7 items 都有
[discuss]marker archive-gate.shCheck 4 驗[discuss]items 都有 evidence trail 或勾選spectra-archive可通過 manual-review hygiene gate
TD-071 — AutoRAG → AI Search API migration
Status: done Priority: critical Resolved: 2026-06-09 — v0.57.1 production deploy 成功(CI run 27185472621 全綠;staging gate + production deploy + smoke test 通過) Discovered: 2026-06-09 — rag-query-rewriting staging acceptance run 全掛(evlog: AutoRAGInternalError: vectorize_filter_not_serializable → fix filter shape → Invalid input) Location: server/utils/ai-search.ts(binding + request shape)、server/utils/knowledge-retrieval.ts(filter construction) Related markers: search @followup[TD-071] in repo Blocks: rag-query-rewriting 6.3-6.6 staging acceptance(retrieval pipeline 全掛,撈不到 retrieval_score)
Problem
Cloudflare 已將 AutoRAG 遷移至 AI Search。舊版 env.AI.autorag(indexName).search({query, filters, max_num_results, ranking_options, rewrite_query}) 在 staging 從 2026-05-04 起 100% 500。
兩階段 root cause(evlog ground truth):
- Filter shape(已 fix v0.56.6):
{type:'eq', key, value}compound filter 被 Vectorize 拒絕 →vectorize_filter_not_serializable - Search request shape(已 fix v0.56.7 移除 filter,但仍 500):AutoRAG
.search()即使傳空 filter 也報Invalid input— 整個 request payload shape(max_num_results/ranking_options/rewrite_query)不再被接受
新 API 形式(per Cloudflare docs 2026-06):
- Binding:
env.AI_SEARCH.get(instanceId)而非env.AI.autorag(indexName) - Request:
{query, ai_search_options: {retrieval: {max_num_results, match_threshold, filters}}} - Filter:
{and: [{eq: {"metadata.category": "..."}}]}或 implicit key-value{folder: "..."} - Response:
chunksarray 而非dataarray
Fix approach
- [x] 改
ai-search.ts:binding 對象、request shape、response parsing,使用AI_SEARCH.get(instanceId).search() - [x] 改
knowledge-retrieval.ts:不再輸出 legacy AutoRAG filter shape;D1 post-search verification 仍是權限真相 - [x] 改 Web chat / MCP ask / MCP search callsite 與 accessor:
AI保留給 Workers AI.run(),search 改AI_SEARCH - [x] 改 wrangler/deploy/render config:production/staging 同時保留
AI與AI_SEARCH - [x] 改 unit/integration/acceptance fakes(mock AutoRAG binding → mock AI Search namespace binding)
- [x] staging redeploy + 驗證 chat 200 + retrieval_score 有值(2026-06-09 deploy Version
8a518f2b;D1 evidence:retrieval_score=0.51) - [x] production deploy v0.57.1(2026-06-09 CI run 27185472621;staging gate + deploy-production + smoke-test 全綠)
Current evidence
- Local unit gate:
3 passed / 21 testsforai-search,knowledge-retrieval,require-ai-binding - Integration subset:
5 passed / 27 testsfor chat route, MCP ask/search, MCP route/access mocks - Full local gate:
pnpm typecheckno errors;pnpm buildcomplete;test:unit128 passed / 806 tests;test:integration90 passed / 485 passed / 1 skipped - Static audit: no
.autorag(,method: 'autorag', orresponse.datahits inserver test wrangler*.jsonc .github/workflows/deploy.yml; remaining legacy-shaped keys are internal adapter input and tests asserting translation - Wrangler rendered staging dry-run:
.output/server/wrangler.staging.jsonincludesenv.AI_SEARCH (default) AI Search Namespace,env.AI AI, andNUXT_KNOWLEDGE_AI_SEARCH_INDEX ("agentic-rag-staging") - Staging deploy (2026-06-09): Version
8a518f2b-3146-4e60-8756-b51b28819b81;env.AI_SEARCH (inherited) AI Search Namespaceconfirmed - Staging D1 evidence: query_log
2508e06cat2026-06-08T20:25:38Z→decision_path=judge_pass_refuse、retrieval_score=0.51(AI Search 成功回傳 score) - MCP SSE acceptance: 4/5 pass(initialize / notifications / tools/list / SSE channel ✅;askKnowledge fail 根因是
rewriter_status=fallback_error,屬 rag-query-rewriting scope) - Production deploy (2026-06-09): v0.57.1 tag push 觸發 deploy workflow run 27185472621;verify-ci-gate ✅ → verify-staging-gate ✅ → deploy-production ✅ → smoke-test ✅ → notify ✅。
curl https://agentic.yudefine.com.twHTTP 200
Acceptance
- Staging
/api/chat回 200(不再 500) query_logs.retrieval_score有非零值query_logs.rewriter_status正確寫入(disabled / success / fallback_*)- Local unit/integration gates 全綠(
806unit tests passed;485integration tests passed /1skipped) rag-query-rewritingacceptance 6.4-6.5 可跑出 retrieval_score 分布