Skip to content

[PR] fix(storage): normalize roleless rows so unified ingest stops throwing on node:sqlite (#4407) - #5261

Open
ranxianglei wants to merge 2 commits into
masterfrom
2026-10-31_fix-unified-store-roleless-bind
Open

ranxianglei wants to merge 2 commits into
masterfrom
2026-10-31_fix-unified-store-roleless-bind

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #4407

What

On the dsh web lane, a stored message whose view row has no role field made the unified store's messages insert bind undefined into a NOT NULL column. node:sqlite rejects that with TypeError: Provided value cannot be bound to SQLite parameter 6.; three consecutive save failures then latched the process into "unified store auto-disabled" (degraded) until restart — same amplifier as #4341, different trigger (wrong-type bind, not a lock).

Fix

Normalize the role column at the source (both ingesters) and at every bind site (sqlite + mysql insert/restore/verify): a roleless row stores the sentinel "unknown" in the NOT NULL column while its verbatim content bytes stay untouched. Every wire/render/recovery reader re-parses content and drops non-string-role rows, so the sentinel never reaches the model line. Insert errors now carry session_id/seq/msg_id so the degrade relay names the failing row.

Behavior change (old → new)

  • Old: a roleless stored row → INSERT throws → after 3 saves the whole unified store latches off until restart (degraded mode; legacy dual-write keeps folds byte-exact but unified-only features unavailable).
  • New: the row lands with role="unknown" in the metadata column (content verbatim unchanged); it is dropped by every reader exactly as before, and the store no longer latches. No currently-working session is affected (they have no roleless rows).

Tests

  • New regression test tests/issue4407-roleless-bind.test.ts: red pre-fix with the exact param-6 TypeError, green post-fix. Asserts no throw, sentinel column, verbatim content, real roles preserved, and projection verify passes.
  • Targeted storage suites green locally (unified / backend-contract / acp-invariants / corpus). Full-suite verdict = remote CI.

Out of scope

Per-row quarantine instead of the whole-store latch (suggestion #3 in the issue) changes shared failure-amplifier semantics with #4341 and is a separate behavior decision — not changed here.

@ranxianglei ranxianglei changed the title fix(storage): normalize roleless rows so unified ingest stops throwing on node:sqlite (#4407) [PR] fix(storage): normalize roleless rows so unified ingest stops throwing on node:sqlite (#4407) Oct 30, 2026
@ranxianglei ranxianglei added bug Something isn't working dsh p1 scope/storage labels Oct 30, 2026
@ranxianglei
ranxianglei force-pushed the 2026-10-31_fix-unified-store-roleless-bind branch from 30e4196 to 6e39a95 Compare October 30, 2026 22:32
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-31_fix-unified-store-roleless-bind (155b375)

Latest build — install this exact version

npm install -g billion-context@0.0.9178-pr.5261.5d22108

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
155b375 billion-context@0.0.9178-pr.5261.5d22108 build
d8be2b8 billion-context@0.0.9164-pr.5261.c2be2f6 build
cca1f46 billion-context@0.0.9160-pr.5261.ed170cd build
af27bc4 billion-context@0.0.7366-pr.5261.7e287e2 build
feee5bf billion-context@0.0.7290-pr.5261.a8342d3 build
6e39a95 billion-context@0.0.7246-pr.5261.61116ba build
30e4196 billion-context@0.0.7243-pr.5261.bf334cf build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr5261.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei
ranxianglei force-pushed the 2026-10-31_fix-unified-store-roleless-bind branch 3 times, most recently from 3ff9e40 to af27bc4 Compare October 31, 2026 03:43
@ranxianglei
ranxianglei force-pushed the 2026-10-31_fix-unified-store-roleless-bind branch from af27bc4 to cca1f46 Compare November 11, 2026 14:50
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Conflict resolution complete — new head cca1f464ab, GitHub now reports mergeable: true (state blocked = required checks/reviews still pending, no more conflict).

What I did this run:

  1. Diagnosed the divergence. The branch carried exactly ONE unique commit (af27bc45c9, the unified store latches off on dsh web lane: unguarded undefined message.role throws node:sqlite "cannot be bound to SQLite parameter 6" #4407 fix) sitting on top of an Oct-31 master line whose tip (491ac1dede) is no longer an ancestor of current master — master moved ~360 commits ahead and its history was restructured, so a plain merge would have dragged in a large abandoned side line. Correct move: replay just the one substantive commit onto fresh master.
  2. Rebased onto current master (31684f368e — master advanced twice during the run, I tracked it to the newest tip). Four conflicts, all the same shape: master's feat: Support precise timestamps for each message, allowing the model to recognize which messages are outdated and which | feat: 给每个消息支持精确时间,以让模型认识到什么消息是过期的什么是当前的新的 #4313 added a ts column to the messages INSERTs while this PR rewrote the same bind sites for role normalization + row-identity error wrapping. Resolved every one as the union of both intents — ts column kept AND normalizeMessageRole + try/catch wrapping applied:
    • src/storage/store.ts:1098 (sqlite ingest insert), src/storage/store.ts:2188 (export restore)
    • src/storage/mysql.ts:782 (mysql ingest insert), src/storage/mysql.ts:1891 (export restore)
    • Auto-merged hunks verified intact: UNSET_MESSAGE_ROLE/normalizeMessageRole definitions (src/storage/store.ts:168-170), both ingesters (src/storage/ingest.ts:195,340), both verify-projection comparisons (store.ts:2699, mysql.ts:2311).
  3. Verified before pushing: tsc --noEmit clean on the rebased head; targeted regression tests/issue4407-roleless-bind.test.ts passes (1/1). Final diff stat is byte-identical to the original commit (4 files, +131/−17) — nothing lost in the rebase. Pushed with --force-with-lease pinned to the old head (re-fetch confirmed no concurrent worker touched the branch).
  4. Inherited-red check: per CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 (floor 1242, today 10:24Z) master is green and ci-fixes carries zero unique commits — no ci-fixes merge needed. Checks on the new head are queued/running; if any go red I'll pick them up in the next attempt.

七问:

  1. 是什么问题:PR 分支与 master 冲突(merge state dirty),无法合并。
  2. 什么原因导致的:分支 10-31 切出后 master 前进了约 360 个提交且历史经过重组——分支的旧基线不再是 master 祖先;同时双方改了同一批 storage bind 点(feat: Support precise timestamps for each message, allowing the model to recognize which messages are outdated and which | feat: 给每个消息支持精确时间,以让模型认识到什么消息是过期的什么是当前的新的 #4313 加 ts 列 vs 本 PR 的 role 归一化+错误包装),产生文本冲突。
  3. 影响范围与严重程度:仅影响本 PR 自身的可合并性,无用户可见行为变化;流程级阻塞,已解除。
  4. 怎么修复的:把分支上唯一实质提交 cherry-pick 到最新 master(31684f3)再 force-push;4 处冲突全部按「双方意图并集」解——保留 master 的 ts 列,同时保留本 PR 的 normalizeMessageRole 与带行身份的异常包装,diff stat 与原提交完全一致,无内容丢失。
  5. 是否有回归问题:逐项审过既有行为——ts 列语义(feat: Support precise timestamps for each message, allowing the model to recognize which messages are outdated and which | feat: 给每个消息支持精确时间,以让模型认识到什么消息是过期的什么是当前的新的 #4313 批量时间戳)在全部 4 个 insert/restore 点原样保留;role 归一化只作用于无 role 的行(undefined→"unknown" 哨兵),有合法 role 的行不受影响;verify projection 两侧用同一归一函数比较,不会误报 corrupt;本地 typecheck 干净、定向回归测试通过。全量判定以 CI 为准。
  6. 是否破坏缓存:不涉及(未触碰缓存/重试/游标/去重策略)。
  7. 是否改了设计方向或引入新配置:没有设计方向变化,没有引入任何新配置。

收工判定:冲突已清、可合并状态已恢复,等 CI 全绿后即可由人工合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

unused-exports fixed — new head d8be2b8eb6, pushed as a plain fast-forward commit (no force; re-fetch confirmed no concurrent worker on the branch).

What I did this run:

  1. Re-verified live first. Mirror cache on the previous head (cca1f464ab) confirmed exactly one failure: unused-exports (test lanes were fail-fast-skipped behind it). Fetched the job log once via API: INTERNAL_ONLY src/storage/store.ts: UNSET_MESSAGE_ROLE.
  2. Diagnosed. The unified store latches off on dsh web lane: unguarded undefined message.role throws node:sqlite "cannot be bound to SQLite parameter 6" #4407 commit exported the sentinel constant UNSET_MESSAGE_ROLE = "unknown" (src/storage/store.ts:168), but repo-wide only its own file references it (inside normalizeMessageRole) — the regression test asserts the literal "unknown", nothing imports the symbol. Textbook INTERNAL_ONLY; latent since the original commit (its old-baseline CI matrix never completed green), surfacing now that the gates actually ran post-rebase.
  3. Fixed with the gate's recommended one-word de-export (export const → const), matching project precedent for this exact failure class (e.g. the ClientBuildCheck de-export). Nothing else touched.
  4. Verified before pushing: local node scripts/check-unused-exports.mjs → ✓ unused-export gate: clean (12 waived); targeted regression tests/issue4407-roleless-bind.test.ts still 1/1 green. Also logged the one-line conclusion in CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 per CI-coordination policy.
  5. Live state after push: mirror cache now shows head d8be2b8eb6 with 0 failed / 10 pending — the new run is in flight; I have not yet observed the remote unused-exports turn green (it's still executing), but the identical gate passes locally on this exact tree. If anything else goes red, next attempt picks it up.

七问:

  1. 是什么问题:新 head cca1f46 上 unused-exports 门禁红,并因 fail-fast 连带跳过了整个 test 层。
  2. 什么原因导致的:原提交把哨兵常量 UNSET_MESSAGE_ROLE(src/storage/store.ts:168)导出了,但全仓只有同文件内部的 normalizeMessageRole 引用它(回归测试断言的是字符串字面量,不 import 该符号)——典型 INTERNAL_ONLY。该问题在原提交时就潜伏着(旧基线上 CI 矩阵从未跑完过),rebase 后门禁真正执行才显形。
  3. 影响范围与严重程度:流程级门禁失败,无用户可见行为变化;去掉 export 不改变任何运行时语义(常量仍在模块内正常使用)。
  4. 怎么修复的:一词去 export(门禁自身推荐的标准修法,与项目先例一致),独立小修 commit d8be2b8;本地门禁 ✓ clean (12 waived)、定向回归测试 1/1 绿后才推送。
  5. 是否有回归问题:逐项审过——唯一引用点(store.ts:170 的 normalizeMessageRole)是模块内使用,不受影响;normalizeMessageRole 仍导出,消费方 ingest.ts / mysql.ts 不变;wire/storage 格式、错误处理路径零改动。
  6. 是否破坏缓存:不涉及。
  7. 是否改了设计方向或引入新配置:没有;无新配置。

收工判定:修复已推、本地门禁全绿,远端新一轮 check 进行中(当前 0 红);若后续再红由下一次唤醒继续处理。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Closing status — both blockers cleared, full matrix green.

Live mirror-cache read this run (head d8be2b8eb6, fetched 15:26Z): 30/30 checks completed, 0 failed, 0 pending — including unused-exports and the previously fail-fast-skipped test lanes. No force-pushes beyond the one sanctioned conflict rebase; the gate fix went out as a plain fast-forward commit.

Session total for this PR:

  1. Conflict → single substantive commit rebased onto current master, 4 conflicts resolved union-of-intent (ts column kept + role normalization/error wrapping kept), head cca1f464ab, merge state dirty→cleared.
  2. unused-exports → UNSET_MESSAGE_ROLE de-exported (one word, gate-recommended), head d8be2b8eb6, gate green locally and remotely.
  3. Conclusion logged in CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 per CI-coordination policy.

Note: merge state currently reads behind (master moved again since my rebase base 31684f368e) — not a conflict, nothing blocked; if master drift causes fresh conflicts later, the next wake-up handles it.

收工判定:冲突已清、CI 全绿(30/30),PR 处于可合并状态,等人工合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Progress report after forced process restart (session had gone idle after completing the work — nothing was mid-flight; no lost progress).

Re-verified LIVE in this run (mirror cache fetched 15:44Z):

  • Head still d8be2b8eb6 (rebase commit cca1f464ab + gate de-export commit on top of master 31684f368e).
  • 30/30 checks completed, 0 failed, 0 pending — both original blockers (merge conflict, unused-exports red) remain cleared.
  • Merge state reads behind: master advanced again (31684f368e → c26c31ace4) while this PR sat green. That is NOT a conflict (state would be dirty if so) and does not block merging — GitHub still computes the branch as cleanly mergeable. I did not chase the moving master: re-rebasing every few minutes is a loop, and the next wake-up handles it if real conflicts ever appear.

No open work, no blockers, nothing waiting on me. PR is ready for human merge.

收工判定:进程重启后已重新实测——CI 全绿(30/30)、无冲突,仅因 master 前进呈 behind 状态(不阻塞合并);等人工合并即可。

ci-check added 2 commits November 12, 2026 01:43
…g on node:sqlite (#4407)

A stored message whose view row carries no `role` field reached the sqlite
insert as undefined and threw "Provided value cannot be bound to SQLite
parameter 6", latching the process into degraded mode after three saves on
the dsh web lane.

Normalize the role column at the source (both ingesters) and at every bind
site (sqlite + mysql insert/restore): a roleless row stores the sentinel
"unknown" in the NOT NULL column while its verbatim content bytes stay
untouched — every wire/render/recovery reader re-parses content and drops
non-string-role rows, so the sentinel never reaches the model line. The
verify projection compares the re-parsed sentinel against the stored one so
roleless rows are not flagged corrupt. Insert errors now carry
session_id/seq/msg_id so the degrade relay names the failing row.

Regression test: tests/issue4407-roleless-bind.test.ts — red pre-fix with
the exact param-6 TypeError, green post-fix.
@ranxianglei
ranxianglei force-pushed the 2026-10-31_fix-unified-store-roleless-bind branch from d8be2b8 to 155b375 Compare November 11, 2026 17:50
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Merge-state blocker handled — branch rebased onto current master, new head 155b375cf8.

What I found and did this run:

  1. Verified live first. Mirror cache (fetched 16:31Z) showed head d8be2b8eb6 with 0 failed / 0 pending and merge state behind — NOT dirty. Local git merge-tree --write-tree HEAD origin/master also merged clean (zero conflicts), and since my old base 31684f368e, master's new commits (PR [PR] Web Panel: Handoff Documentation and Collapsed Tree Diagram Rendering Upgrade (eWork-Level Rendering, Appendix Sectionin | Web 面板:交接文档与压缩树图渲染升级(eWork 级渲染、附录分节、冻结快照水合) #6260, web/handoff-rendering) touch none of this PR's files (src/storage/store.ts, mysql.ts, ingest.ts). So the "dirty" in the notification was stale — the real condition was base drift.
  2. Advanced the base anyway (per the lane directive and §7.2 "rebase to current master before claiming mergeable"): rebased the two commits cleanly onto current master c26c31ace4 — zero conflicts, nothing modified.
  3. Proved correctness before pushing:
    • rebased final tree is byte-identical to the independent git merge-tree result (a759aab6) — union-of-intent verified mechanically, not by eye;
    • targeted regression tests/issue4407-roleless-bind.test.ts 1/1 green;
    • unused-exports gate clean (12 waived); tsc --noEmit clean.
  4. Pushed with --force-with-lease pinned to the pre-push remote head (re-fetched immediately before; no concurrent worker on the branch) — sanctioned force-push for this conflict-rebase task. Remote now at 155b375cf8.

The previously-green run on d8be2b8eb6 covered a byte-identical tree, so behavior is unchanged; the full matrix will re-run on the new head and the next wake-up picks up anything red.

七问:

  1. 是什么问题:系统报告 PR 与基线冲突(dirty);实测为基线漂移(behind)——master 前进了,分支基线停在旧位。
  2. 什么原因导致的:master 合入 PR [PR] Web Panel: Handoff Documentation and Collapsed Tree Diagram Rendering Upgrade (eWork-Level Rendering, Appendix Sectionin | Web 面板:交接文档与压缩树图渲染升级(eWork 级渲染、附录分节、冻结快照水合) #6260(web 渲染类提交,c26c31ace4)后基线前进,本分支基线仍为 31684f3;通知里的 "dirty" 是滞后信号——镜像缓存(16:31Z)与本地 merge-tree 双重实测均为干净可合并。
  3. 影响范围与严重程度:流程级状态漂移,无代码/用户影响;behind 本身不阻塞人工合并。
  4. 怎么修复的:按车道指令执行冲突 rebase——两个提交干净重放到最新 master(c26c31a),零冲突零改动;最终树与独立 merge-tree 结果逐字节一致,定向回归测试、unused-exports 门禁、tsc 全绿后 force-with-lease 推送。
  5. 是否有回归问题:rebase 未修改任何一行(树等价性已机械证明),既有行为零变化;master 新增提交与本 PR 文件无交集。
  6. 是否破坏缓存:不涉及。
  7. 是否改了设计方向或引入新配置:没有;无新配置。

收工判定:分支已对齐当前 master、内容经多重验证,等新一轮 CI 跑完即可合并;若再红由下次唤醒处理。

@ranxianglei ranxianglei left a comment •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ai: vllm-qwen/qwen3.8-27b)

Verdict: APPROVE — posted as a comment review because GitHub refuses a formal approval from the PR’s own account (author == reviewer here).

Independent end-to-end review of head 155b375cf8 (the branch sits directly on current master c26c31ace4; no base drift). Mirror cache at this sha: 30/30 checks, 0 failed (12 skipped by design).

Verified myself rather than taking the author's word:

  • Red→green reproduced locally: pre-fix sources fail the new regression test with the byte-exact field error (TypeError: Provided value cannot be bound to SQLite parameter 6.); PR head passes 1/1. tsc --noEmit clean; unused-exports gate clean (12 waived).
  • Root cause, not a mask: both planners normalize parsed.role at the source; all six messages bind sites (sqlite+mysql ingest/restore) and both verify-projection comparisons use the SAME helper, so the stored column and the re-parsed content can never disagree. The issue's suggested role ?? null was correctly rejected — role is NOT NULL in both DDLs (schema.ts:162, mysql.ts:160), so null would just trade the TypeError for a constraint failure.
  • Sentinel never reaches the model line: independently confirmed both reader guards — src/server/snapshot-render.ts:127 and src/persist.ts:4404 drop non-string-role rows after re-parsing the verbatim content; the test pins that content bytes stay role-less.
  • Fail-fast preserved: insert errors rethrow with row identity, original message kept as a suffix; no production consumer matches the old error string (shadow dedup/lastError gets strictly better context).
  • No residual same-class gap: blocks start_ref/end_ref are planner-normalized to number | null (ingest.ts:422-423); the new sqlite ref/refNum/kind defaults align it with the guards mysql already had (MessageRow types these non-optional, so typed callers see zero change); the MySQL NOT NULL DDL means no legacy NULL-role rows can exist for verify to misflag.
  • The test has teeth: real builtin node:sqlite engine, real planIngest → openBackend → ingestSession; asserts no throw, both rows land, verbatim content (no role key), sentinel column, real role preserved, verifyProjections ok. A regression of this bug class goes red.

One accuracy note (non-blocking, no code impact): the PR body's "latches off until restart" describes the reporter's 0.1.204 build. On current master the amplifier already changed (#5627 removed self-disable — pinned by tests/issue5627-storage-failure-forensics.test.ts:116), so on master the user-visible delta is: every save of an affected session rolled back its ENTIRE sidecar transaction (unified-only features dead for that session — the cold-load ~436k-token replay from #4407 floor 2) → those saves now commit. Root cause and fix are identical either way; the disclosure stands for the reported version.

Scope/conventions: host-side src/storage only — no kernel/, no config surface, no schema/wire/persistence-format change, both compression modes unaffected (storage sits below the wire), clean diff (4 files, +131/−17, every line on-purpose), single linear history on current master. §7.4 auto-merge criteria satisfied; human merges as usual.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] (ai: vllm-qwen/qwen3.8-27b)

Review complete — verdict: APPROVE, posted on GitHub PR #5261 as a comment review (review link). One mechanical caveat: GitHub refuses a formal approval from the PR's own account (author == reviewer here), so mergeable_state stays blocked until a human approves — the review text above carries the full verdict and justification. Head 155b375cf8 sits as two commits directly on current master c26c31ace4 (no base drift); mirror cache at this sha: 30/30 checks, 0 failed (12 skipped by design).

What I did independently (not reusing prior reviews):

  • Read the PR body, linked issue unified store latches off on dsh web lane: unguarded undefined message.role throws node:sqlite "cannot be bound to SQLite parameter 6" #4407 (incl. floor 2 field evidence), and the full 4-file diff (+131/−17) line by line.
  • Reproduced red→green locally myself: pre-fix sources fail the new regression test with the byte-exact field error TypeError: Provided value cannot be bound to SQLite parameter 6.; PR head passes 1/1. Local tsc --noEmit clean; unused-exports gate clean (12 waived).
  • Audited every existing behavior on the touched paths (see 八问 below) and independently confirmed the two reader guards that keep the sentinel off the model wire (src/server/snapshot-render.ts:127, src/persist.ts:4404).
  • Checked for same-class residual gaps (blocks refs, other positional binds, MySQL legacy data) — none remain.

No small findings needed an on-the-spot fix commit; nothing routed to needs-decision.

One accuracy note on the PR body (non-blocking, no code impact): its "latches off until restart" wording describes the reporter's 0.1.204 build. On current master the failure amplifier already changed (#5627 removed self-disable — pinned by tests/issue5627-storage-failure-forensics.test.ts:116), so on master the user-visible delta is narrower but real: every save of an affected session rolled back its ENTIRE sidecar transaction (unified-only features dead for that session — the cold-load ~436k-token replay from #4407 floor 2) → those saves now commit. Root cause and fix are identical either way; the disclosure stands for the reported version.

八问

  1. 是什么问题:dsh web 车道下,会话历史里某条视图行没有 role 字段时,统一存储把 undefined 绑进 messages.role(NOT NULL 列),node:sqlite 抛 param-6 TypeError,该会话每次保存的整笔 sidecar 事务回滚——统一存储侧永远落不进这个会话的任何行,冷加载/视图恢复随之坏掉(报告者现场:重启后首请求重放 ~436k tokens,preflight 全拒)。
  2. 什么原因导致的:根因在生产端——两个规划器(planIngest/planIngestDelta)把原始视图 JSON.parse 后的 parsed.role 直接透传,dsh raw-tail 存在无 role 条目 → undefined 一路带到 insert 第 6 参。issue 建议的 role ?? null 修不了(role 列 sqlite/mysql 双端 DDL 均 NOT NULL:null 只会把 bind TypeError 换成约束失败);失败放大器(旧版 3 次 latch / 现版无限重试)只是放大器不是病因。
  3. 影响范围与严重程度:仅 dsh web 车道且会话含 roleless 行的场景触发;触发后该会话统一存储能力整体不可用(视图恢复/存储 gc/冷加载),legacy 双写保字节一致、非数据丢失。按 owner 2026-10-19 修订属「会话无法恢复」类 → p1(标签已在)。
  4. 怎么修复的:源头(两规划器)+ 全部 6 处 bind 位(sqlite/mysql × insert/restore)+ 两处 verify-projection 比较,统一过同一个归一函数:roleless 行落哨兵 "unknown",content 逐字不变;insert 报错带上 session_id/seq/msg_id,降级中继能点名失败行。是疏(消灭 undefined 的生产点),不是堵。
  5. 是否有回归问题(触碰路径既有行为逐项清点):正常有 role 行=恒等映射零变化;roleless 行=从「整笔回滚+失败累积」变「正常落库+计数复位」严格变好;错误文案富化保留原始消息为后缀、fail-fast 未变吞错、无消费者匹配旧错误串;sentinel 永不出模型线(snapshot-render.ts:127 与 persist.ts:4404 两道守卫独立核实,测试钉住 content 逐字不变);sqlite ingest 新增 ref/refNum/kind 缺省守卫与 mysql 既有一致(MessageRow 类型层面这些字段非可选,typed 调用方零变化);blocks start_ref/end_ref 规划器已归一为 number|null,无同类暴露;MySQL NOT NULL DDL 排除存量 NULL-role 行被 verify 误报;kernel/、配置面、schema、wire 形状、持久化版本全未碰。
  6. 是否破坏缓存:不涉及——content_hash/CAS/identity 键全用逐字 content(不变),前缀缓存接缝不受影响。
  7. 是否改了设计方向:无——仍在 host 侧 src/storage,哨兵常量是内部实现细节(已按 unused-exports 门禁去 export)。
  8. 是否引入了新的配置:无新 env/config/默认值。

收工判定:代码通过、CI 30/30 绿、无冲突、无待决策项——PR 处于可合并状态,等 owner 人工合并(GitHub PR #5261)。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dsh p1 scope/storage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unified store latches off on dsh web lane: unguarded undefined message.role throws node:sqlite "cannot be bound to SQLite parameter 6"

1 participant