You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Serialization, added at triage: packages/objectql/src/engine.ts carries two stacked open drafts on the transaction paths — #6165 (#5696, base main) and #6171 (#5351, stacked on #6165). Selection skips this issue until #6171 merges; re-verify the join branch against the merged code before implementing.
Findings triage (#4949 discipline): promote — finding → pm:queue + pm:blocked. domain:engine-core retained. Blocked-by: #6171 added to the body (the machine half; selection reads it there, not here).
Stale-premise check (origin/main @ 42af12f, read from the tree). Premise confirmed line-for-line, and one thing the body left implicit is worth making explicit:
ObjectQL.transaction() performs the ADR-0067 D2 join at packages/objectql/src/engine.ts:6665 — const ambient = this.txStore.getStore(); if (ambient?.transaction) { return callback({…}) }. The comment directly above it (:6660–:6664) states the reason in this issue's own terms: a nested open "would NOT be covered by the outer rollback", and an outer batch transaction "must own the one-and-only commit/rollback for every write made through nested helpers".
ScopedContext.transaction() at :7278 reaches driver.beginTransaction() at :7294 with no ambient read in between. Verified by direct read of :7278–:7300.
The asymmetry is not declared. The method's own TSDoc at :7269–:7276 claims it "Carries BOTH of ObjectQL.transaction's declared caveats (ADR-0119 D1)" and enumerates them — default-datasource-only, and the silent degrade when the driver has no beginTransaction. Join is not among them and is not mentioned anywhere in the method. So this is an overlooked divergence, not a documented boundary — which is precisely what separates it from its sibling 事务句柄没有属主可查,同源校验对「引擎没开过的句柄」只能弃权(#5351 的已知边界) #6167, where the boundary is declared (ADR revision plus a pin named "declared, not overlooked") and which is held this round.
Why queue rather than hold — this is the restore-invariant shape, not an observation:
The invariant is accepted and written down (ADR-0067 D2), and one of the two implementations of the same thing already enforces it. The gap is not "should we guarantee this" but "one face forgot to".
The failure mode is durability, not function. The inner transaction commits, the outer rolls back, the inner writes stay — and nothing reports anything. Plus the connection cost D2 exists to avoid, which deadlocks on a single-connection SQLite pool.
The body grades its own reachability honestly (needs a sandbox hook body calling ctx.api.transaction(), itself triggered inside an engine.transaction()), and I accept that measurement. But reachability sets priority, not queue-vs-hold: a silently violated accepted invariant with a named landing site and a bounded fix is dispatchable work. No priority:p0 — the trigger is not demonstrated in a live deployment.
PM mechanism assumption — must be measured, falsification welcome. The body flags that a join here must not mistake the sandbox trio's explicitly-threaded handle for an ambient one (the #6167 coupling). Reading :950–:961, the txStore scope doc states the trio "does not use this store at all". If that holds, a txStore.getStore() read cannot see a trio handle and the risk does not arise. This is my read of the mechanism, not a ruling — verify it first; if the trio reaches txStore by some path, the join needs a discriminator and this issue's scope grows.
Serialization — this is why it lands blocked rather than ready. Two open drafts are stacked on this exact file, both created within the hour, both from the batch that filed this finding:
#6171 is stacked on #6165's head, so they land in order. Dispatching now would put a third author into engine.ts beside ~280 lines of in-flight change to the transaction paths themselves. Hence Blocked-by: #6171; the unlock sweep returns it when that merges. Re-verify against the merged code before implementing — the join branch may need to sit differently once #6171's same-origin scope machinery is in place.
Dedup: three-repo scan of open issues and PRs. #5696 / #5351 / #4619 are this surface's parents and are already cited in the body; #6167 is the declared-boundary sibling, held this round. No independent duplicate.
Activity
claude commentedon Aug 7, 2026
Findings triage (#4949 discipline): promote —
finding→pm:queue+pm:blocked.domain:engine-coreretained.Blocked-by: #6171added to the body (the machine half; selection reads it there, not here).Stale-premise check (
origin/main@42af12f, read from the tree). Premise confirmed line-for-line, and one thing the body left implicit is worth making explicit:ObjectQL.transaction()performs the ADR-0067 D2 join atpackages/objectql/src/engine.ts:6665—const ambient = this.txStore.getStore(); if (ambient?.transaction) { return callback({…}) }. The comment directly above it (:6660–:6664) states the reason in this issue's own terms: a nested open "would NOT be covered by the outer rollback", and an outer batch transaction "must own the one-and-only commit/rollback for every write made through nested helpers".ScopedContext.transaction()at:7278reachesdriver.beginTransaction()at:7294with no ambient read in between. Verified by direct read of:7278–:7300.:7269–:7276claims it "Carries BOTH ofObjectQL.transaction's declared caveats (ADR-0119 D1)" and enumerates them — default-datasource-only, and the silent degrade when the driver has nobeginTransaction. Join is not among them and is not mentioned anywhere in the method. So this is an overlooked divergence, not a documented boundary — which is precisely what separates it from its sibling 事务句柄没有属主可查,同源校验对「引擎没开过的句柄」只能弃权(#5351 的已知边界) #6167, where the boundary is declared (ADR revision plus a pin named "declared, not overlooked") and which is held this round.Why queue rather than hold — this is the restore-invariant shape, not an observation:
:6665branch and report{ owned: false }— exactly what [spec] engine.transaction 契约收紧:opts.requirefail-closed、跨驱动拒绝、owned-vs-joined 信号(#4619 的契约半边,维护者已批 P2) #5696'sownedsignal was added to express. The issue notes that this face currently reportstrueunconditionally, so today the signal is honest about a behaviour that is not.The body grades its own reachability honestly (needs a sandbox hook body calling
ctx.api.transaction(), itself triggered inside anengine.transaction()), and I accept that measurement. But reachability sets priority, not queue-vs-hold: a silently violated accepted invariant with a named landing site and a bounded fix is dispatchable work. Nopriority:p0— the trigger is not demonstrated in a live deployment.PM mechanism assumption — must be measured, falsification welcome. The body flags that a join here must not mistake the sandbox trio's explicitly-threaded handle for an ambient one (the #6167 coupling). Reading
:950–:961, the txStorescopedoc states the trio "does not use this store at all". If that holds, atxStore.getStore()read cannot see a trio handle and the risk does not arise. This is my read of the mechanism, not a ruling — verify it first; if the trio reaches txStore by some path, the join needs a discriminator and this issue's scope grows.Serialization — this is why it lands blocked rather than ready. Two open drafts are stacked on this exact file, both created within the hour, both from the batch that filed this finding:
engine.tsmainclaude/issue-5696-transaction-contract#6171 is stacked on #6165's head, so they land in order. Dispatching now would put a third author into
engine.tsbeside ~280 lines of in-flight change to the transaction paths themselves. HenceBlocked-by: #6171; the unlock sweep returns it when that merges. Re-verify against the merged code before implementing — the join branch may need to sit differently once #6171's same-origin scope machinery is in place.Dedup: three-repo scan of open issues and PRs. #5696 / #5351 / #4619 are this surface's parents and are already cited in the body; #6167 is the declared-boundary sibling, held this round. No independent duplicate.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
baozhoutao commentedon Aug 7, 2026
解除阻塞(执行席 PM 解锁清扫):
Blocked-by: #6171已于 2026-08-07 08:58Z MERGED(事务同源校验 + carve-out 落地),摘pm:blocked回派发队列。注意其面在engine.ts事务区域 —— 与本车道在飞/待发单的同文件串行纪律照常适用,排批时核区域。Generated by Claude Code
baozhoutao commentedon Aug 7, 2026
认领(执行席 PM,engine-core 车道,第 13 轮)
session_019Q7oc7ASjh8yxyS3Yz78Weclaude/issue-6168-scoped-transaction-join../objectstack-issue-6168packages/objectql/src/engine.ts仅 ScopedContext.transaction / 事务区(fix(objectql): update 剥离作用于调用方提交的值,不再连坐抹掉 beforeUpdate hook 的写入 (#5591) #6343 已落,链上轮到本区)+ 测试 + changeset;⛔ 不触 update 剥离区(刚落)、自增区、事务句柄没有属主可查,同源校验对「引擎没开过的句柄」只能弃权(#5351 的已知边界) #6167 的不可归属句柄面(独立跟进)Generated by Claude Code
baozhoutao commentedon Aug 7, 2026
ACCEPT(执行席 PM 验收,第 13 轮)
PR #6403 验收通过,已翻 ready + auto-merge。记档:① 回调面 join 支补齐,回滚持久性钉子(D2 残留形状)与单连接池钉子(拒绝建模代理挂起 +
begins.length===1的直接测量,建模选择如实声明)双双在案;② 与 #6171 同源校验交互不变性实测(join 穿的是同一句柄对象,tx === store.transaction恒真 → 判定按外层属主);③ 可达面修正:issue 正文的「沙箱 hook」实为进程内 body(HookContext/action api/直接 createContext),QuickJS 面走三件套 —— VM 侧同款缺口已立 #6406(与 #6167 不同件事、同片面,排批串行);④require:true遇 ambient 改为 join 满足(与引擎面一致,契约测试钉住);⑤ #6167 定价实测不变、且 #6167 先落会让 #6406 变便宜的正向依赖已记档;⑥ 显式句柄 vs ambient 的优先级问题按声明式边界留给 #6167/#6406 的契约决定,未擅动。Generated by Claude Code
fix(objectql): ScopedContext.transaction 补齐 ADR-0067 D2 的 ambient join (