fix(objectql)!: 事务句柄不再跨数据源穿透 —— 业务写响亮拒绝、系统账本移出事务落盘 (#5351) - #6171
Conversation
…losed and an `owned` signal (#5696) `IObjectQLEngine.transaction` declared two degradations as part of its meaning (ADR-0119 D1): default-driver-only routing, and a silent fallback to "no transaction, no rollback" on a driver without `beginTransaction`. #4619 made both audible (PR #5724). This lands the first two of #5696's three tightenings, each opt-in, with every existing caller's behaviour unchanged: - `opts.require: true` throws `TransactionUnsupportedError` instead of degrading — refused BEFORE the callback runs, so nothing is written when the caller finds out. Generalizes `batchData`'s atomic gate (ADR-0119 D4). - the callback's second argument carries `owned`: true when this call opened the transaction, false when it JOINED an outer one (ADR-0067 D2) or ran on the degrade path where there is no transaction to own. Both are honoured on `ScopedContext.transaction` (`ctx.api.transaction`) too — a second implementation of one primitive must not become a second dialect. The contract TSDoc is corrected on a point measurement disproved: writes routed off the transaction's datasource were NOT "written outside it", they were handed the owner's transaction handle and executed on the wrong connection (#5351). The TSDoc now states that, plus the two decided semantics landing next: business writes refused across drivers, and system ledgers (`lifecycle.class` of audit/telemetry/event) carved out to execute outside the transaction. `@objectstack/core`'s `EngineWithTransaction` is typed FROM the contract rather than transcribed from it — the hand-copy had already started to drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…nsaction-contract
…rred artifact) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ness writes, carve out system ledgers (#5351, #5696) `buildDriverOptions` lifted the ambient transaction handle onto EVERY driver call without asking which driver was about to receive it, so a write routed elsewhere was handed the DEFAULT driver's handle and executed on the wrong connection. Measured on a real boot (#5351): sys_audit_log, routed to the dedicated telemetry datasource by ADR-0057 §3.6, took 52 insert attempts, 50 succeeded, and the 2 failures were exactly the 2 whose stack carried a knex `trxClient.query` frame — `no such table` against the primary database. Every audited write performed inside a transaction lost its compliance row, silently, with no retry. Per the 2026-08-06 maintainer ruling, three changes that had to land together: - a transaction handle is threaded only to the driver that OWNS it, compared by instance identity via TransactionScope (#5724). Reads covered too — they had the same defect and no diagnostic at all. - a cross-driver BUSINESS write inside a transaction is REFUSED (CrossDatasourceTransactionWriteError), at the top of insert/update/delete before any hook, default or validation runs. This is #5696 point 2; it is not cross-driver atomicity, which needs two-phase commit and stays out of scope. - append-only SYSTEM LEDGERS (lifecycle.class audit/telemetry/event) are CARVED OUT and executed outside the transaction, so the compliance row lands. They survive a rollback — the orphan row is the accepted cost: for an append-only ledger a spurious row is reconcilable, a missing row for a committed write is not. Refusing them is not available even in principle, since the audit hook's try/catch turns any refusal back into a dropped row. The `error`-level split diagnostic PR #5724 added retires with the defect it described; the carve-out is noted at `debug`, once per transaction per datasource, because it is now declared behaviour that fires on every audited transactional write in a split deployment. Single-datasource deployments see no change of any kind. ADR-0067 and ADR-0119 carry the revision, including the gate's declared limit: handles the engine never opened cannot be attributed to a driver, so the gate declines to judge them rather than guess (filed as #6167). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…-audit-same-origin Conflict: packages/objectql/src/engine.ts import block — both sides added an import next to the same line (#5351's transaction-errors, main's summary-aggregate). Both kept; no logic overlap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ransaction signature with the contract (#5696) CI's `TypeScript Type Check` went red where a scoped `pnpm --filter … typecheck` could not: @objectstack/metadata-protocol has no `typecheck` script, so its errors are only ever seen by the DEBT ledger's `--re-measure`, which reported 63 -> 70 (+7). All seven were one shape. Six test doubles stand in for `SysMetadataEngine`, whose `transaction?` member is typed FROM the contract (`IObjectQLEngine['transaction']`), and each declared its callback as `(ctx: any) => Promise<T>` — one parameter, from before the callback gained its `info` argument. A double may be narrower than the producer; it may not contradict it. Each now declares `(ctx: any, info: { owned: boolean })` and passes `{ owned: true }` at the call — the honest value, since every one of these doubles OPENS the transaction it stands in for, which is exactly what `ObjectQL.transaction` reports on its own open branch. Re-measured: 63 raw errors, the frozen count, with zero transaction-related errors remaining. `check-type-check-coverage --re-measure` reports no upward drift. metadata-protocol: 49 files / 502 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ntract' into claude/issue-5351-txn-audit-same-origin
…e TEST_DEBT re-measure (#5696) objectql's test layer is excluded from its own `tsconfig.json`, so `pnpm --filter @objectstack/objectql typecheck` never reads these files — only the ledger's `--re-measure` does, by synthesizing a config with the test globs unexcluded. The new file arrived with 7 errors nothing local reported. Two shapes, both fixed rather than absorbed: - `registry.registerObject(x as any)` — `packageId` is not optional (TS2554). - `promise.catch((e) => e as E)` types the result `E | <resolved type>`, so every property read on it is TS2339. Replaced by a `rejection<E>()` helper that narrows to the rejection AND throws if the call did not reject at all — which the bare `.catch()` would have let pass silently as a green test. Re-measured: zero errors in this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…re-measure (#5351) Same blind spot as #5696's contract test: objectql excludes its tests from `tsconfig.json`, so `pnpm --filter … typecheck` reads none of these files and only the ledger's `--re-measure` does. The new same-origin file arrived with 15 errors, which the ratchet caught as TEST_DEBT 355 -> 370. Three shapes: - `registry.registerObject(x as any)` — `packageId` is not optional (TS2554); - `promise.catch((e) => e as E)` types the result `E | <resolved type>`, so every property read is TS2339. Replaced by a `rejection<E>()` helper that narrows to the rejection AND throws if the call did not reject at all — which the bare `.catch()` would have let pass silently as a green test; - callbacks over the driver double's `writes` were implicitly `any` (TS7006), since the double itself is `any`. The shape now has a name, `RecordedWrite`. Re-measured: objectql's test layer reports 344 against a recorded 355 (-11) — zero errors in any of the three transaction test files, and the two pre-existing `registerObject` arity errors in the observability file are gone with them. `check-type-check-coverage --re-measure`: 34 entries, none above its recorded number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ntract' into claude/issue-5351-txn-audit-same-origin
…nsaction-contract Conflict: packages/objectql/src/engine.ts import block — both sides added an import next to the same line (#5696's transaction-errors, main's summary-aggregate). Both kept; no logic overlap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…n/main) into claude/issue-5351-txn-audit-same-origin Two conflicts, both additive-on-both-sides: - packages/objectql/src/engine.ts — the transaction-errors import line. This branch's version is the superset (it also imports CrossDatasourceTransactionWriteError); kept. - scripts/adr-anchors.json — both sides appended a distinct anchor entry (#5351's engine.ts anchor; main's two ADR-0122 alias-convention anchors). Both kept; re-parsed as JSON and re-verified with check-adr-anchors (37 anchored files). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…-audit-same-origin main squash-merged #6165 (the contract half) as 1363084, so this branch's original #6165 commits and main's squashed version are two sources for the same content — the known stacked-PR shape. Resolved semantically, not textually. Three conflicted files, all one pattern: the #6165 content is identical on both sides (it auto-merged wherever only #6165 touched it), and every conflict hunk is #5351's own work sitting ON TOP of that identical base. HEAD taken in each: - packages/objectql/src/transaction-errors.ts (add/add) — main has TransactionUnsupportedError; this branch has it verbatim PLUS CrossDatasourceTransactionWriteError (#5696 point 2). Superset kept. - packages/objectql/src/index.ts — same shape: the second error's export. - packages/objectql/src/engine.ts, two hunks — the transaction-errors import (superset), and the transaction() TSDoc bullet that #5351 rewrote from "routed elsewhere runs OUTSIDE it, reported at error" to the decided behaviour (refuse business writes / carve out system ledgers). main's side is the pre-#5351 text and still names reportWriteOutsideTransaction, which this branch renamed to enforceTransactionOrigin. Verified after resolving: no conflict markers, and the only surviving mention of reportWriteOutsideTransaction is the TSDoc line that deliberately records the rename. Changesets: both survive intact. main's `chore: version packages (rc)` (bd19133) landed BEFORE the #6165 squash, so it consumed neither — #6165's changeset is still on main and this branch's own transaction-same-origin-audit-carve-out.md is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
ACCEPT(执行席 PM 验收,同批 PR 序第二落 / 本席收官单) 核过:① 裁 A 完整落地 —— 同源校验结构半( 后续跟进单:#6167(不可归属句柄收口)、#6168(ScopedContext join)。本 PR 合入后:关 #4619(三点全落)、#5226 补根因指引。 enable auto-merge,CI 全绿即进队列(rc.5 口径)。 Generated by Claude Code |
Fixes #5351
Refs #5696(第 2 点在本 PR 落地)、#4619(母单)、#5724(可发现性半边)
修的是什么(#5351 的真实根因)
buildDriverOptions把 ambient 事务句柄无条件塞进每一次 driver 调用,不问即将收到它的是哪个 driver:于是被路由走的对象拿到的是默认库那条连接的事务对象,knex 的
.transacting(trx)把语句发到错误的库。实测(#5351,一次真实 boot):sys_audit_log被 ADR-0057 §3.6 路由到telemetry数据源,insert 尝试 52 次、成功 50 次、失败 2 次,失败的两次堆栈全部带 knex 的trxClient.query帧,报no such table: sys_audit_log;成功的没有一次带。即:凡是在事务中执行的被审计写入,合规审计行全部静默丢失 —— 业务写成功、接口 200、数据在盘上,只有「谁做的」那一行没了,且无人重试。
契约 TSDoc 原先写这类写入「在事务外执行」。实测比这更糟:它是带着另一个连接的事务对象执行的。PR #5724 用纯 in-memory 双数据源独立复现了同一现象(
expected { __trx: 'primary' } to be undefined),证明缺陷属于事务缝本身,与审计无关、与 SQL 无关 —— 只要有任何一条 mapping 规则把对象路由走。三条新行为(2026-08-06 裁决,必须同时成立)
1. 句柄不再跨驱动。
buildDriverOptions只在解析到的 driver 就是事务属主时才穿句柄,按TransactionScope(#5724)记录的实例身份比对。结构性、不分动词 —— 读操作同样覆盖,它们此前也在错误的连接上跑,而且连诊断都没有。2. 业务写跨驱动 → 拒绝(#5696 第 2 点)。
CrossDatasourceTransactionWriteError(code: 'ERR_CROSS_DATASOURCE_TRANSACTION_WRITE'),在insert/update/delete的最顶端抛出,早于任何 hook / 默认值 / 校验 —— 拒绝时调用方一行都没写。⛔ 这不是跨库原子性:IDataDriver没有两阶段提交,#4619 原文已排除;在第二个 driver 上开伴随事务只会用一个更坏的持久性风险换掉当前这个。3. 系统账本移出事务执行(carve-out)(#5351 裁 A)。
lifecycle.class为audit/telemetry/event的只追加账本不拒绝,而是在自己的连接上、事务之外执行 —— 审计行真正落盘。判别式(裁决要求写明)
对象声明的
lifecycle.class∈ {audit,telemetry,event} —— 不是「它被哪种机制路由走的」。一个用显式
datasource:绑定钉到自己库上的审计账本,是同一种只追加合规账本、有同样的理由被 carve-out;按机制判会让一条合规担保取决于运维恰好用了三种等价配置中的哪一种。该 tuple 只存在一处 ——ObjectQL.SYSTEM_LEDGER_LIFECYCLE_CLASSES—— 由 ADR-0057 §3.6 的路由步骤与本门共同读取,因为两份手抄会差一个 class,而差掉的恰好就是本次要救的那一行。孤儿行语义(裁决明确接受的代价)
carve-out 的行会在业务事务回滚后留下 —— 一条审计行可能描述一次被撤销的写入。方向比数量重要:对只追加的合规账本,一条多余的行是可对账的麻烦(拿业务数据一比即知),而一条已提交写入却缺失的审计行是不可恢复的合规空洞 —— 后者正是今天在发的版本。已按裁决写进 ADR-0067 / ADR-0119 修订,并由测试钉住(不是留给读者推断)。
为什么必须同批(#5696 第 2 点为何不能单独先落)
carve-out 之外只留拒绝,就是「响亮化但不修复」:审计 hook 的
try/catch把拒绝吃成日志,合规行照丢,只是换了条日志。这不是推理 —— 反向验证肢 B 实测到了它,见下。#5724 那条 error 日志的语义调整
跨源写入的
error级诊断随它描述的缺陷一起退休 —— 已经没有「静默穿错连接」可报了:业务写抛错(抛比任何日志都响),系统账本按声明落盘。carve-out 路径改为debug,每事务每数据源一次。理由按 AGENTS.md 的判据:降级后系统是否「看起来正常而声称持久化的东西没落地」?否 —— 审计行落了。它现在是声明过的正常行为,在分流部署里每一次被审计的事务写入都会发生;挂在
error甚至warn上就是该节名的镜像错误,会训练读者跳过真正的持久性告警。持久答案在 ADR-0067/0119,不在日志级别。P1 边界:明确不覆盖,并写进文档
同源校验只判能归属属主的句柄。
TransactionScope覆盖引擎自己开的每一个事务;显式穿回来的trxCtx按句柄身份匹配回 txStore 条目,所以主流显式路径(transaction()给你trxCtx、你以{ context: trxCtx }传回)是覆盖的 —— 调用方不能靠传引擎给它的上下文绕开这道门(已钉测试)。不覆盖(决定,非疏漏):
ScopedContext的离散beginTransaction/commit/rollback三件套(跨setImmediate显式穿句柄,根本不走 txStore),以及外部调用方自带的execCtx.transaction。这类句柄是不透明的 driver 对象,没有任何回指其属主的引用,没有诚实的比较可做。猜(比如「无属主记录就当它属于默认驱动」)两边都会错:一边误拒合法的单库工作,一边把真正被覆盖的写入误判为跨源、悄悄移出事务失去回滚保护。建立在猜测上的保证比明确不覆盖更坏。该路径保持 #5351 之前的行为,已作为决定钉进
engine-transaction-same-origin.test.ts(“does NOT cover a foreign handle passed in from outside — declared, not overlooked”),写进 ADR-0119 修订的「The declared LIMIT」一节,收口需要IDataDriver暴露句柄属主 —— 另立单 #6167。ADR 修订落点
按仓内既有惯例(
Status行加**Amended**+ 文末## Amendment (日期, #单号)段),⛔ 未触content/docs/releases/:docs/adr/0119-plugin-reachable-transactions-and-honest-atomic-batch.md—— 主修订。D1-R1…R5:句柄不跨驱动 / 业务写拒绝 / 系统账本 carve-out 与孤儿行 /opts.require/owned;含「caveat 原文事实性错误」一节(D1 说的「written outside it」经实测不成立)与「declared LIMIT」一节。docs/adr/0067-commit-history-and-rollback-for-ai-authoring.md—— D2 join 语义现在可分辨(owned);Decision-2「commit 不会半落地」明确为单数据源承诺并真正被强制;revertCommit之后审计行留存这件事写明白了,并论证它与本 ADR「历史只追加、revert 是新的正向 commit」的设计中心一致。scripts/adr-anchors.json(PD [WIP] Add Chinese version of the documentation #13)—— 更新objectql-engine.ts的 invariant(旧文本仍在描述被推翻的 caveat),新增packages/objectql/src/engine.ts锚(ADR-0057/0067/0119),把「不许恢复无条件穿句柄、不许加伴随事务」写成下一位作者能读到的话。测试
新增
packages/objectql/src/engine-transaction-same-origin.test.ts(18 例),用真实 #5351 拓扑(primary +telemetry专用数据源 + 一条把普通业务对象路由走的 mapping 规则)。driver 替身开出的句柄自带自己的名字(形如{ __trx: 'primary' }),所以句柄跑错 driver 一眼可见:options.transaction为undefined(断言undefined而非「不是 primary 的」—— 这里本就不该有任何句柄)、三种 class 全覆盖、update/delete 同样 carve-out、业务回滚后审计行保留(孤儿行钉死)、debug每事务每数据源一次且error/warn计数为 0、新事务重新计数;code+ 四个字段 + 消息含两条修法 +Nothing was written.;两个库都没写且外层已回滚;update/delete 同样拒绝;join 的嵌套事务按外层属主判定后拒绝;ScopedContext.transaction同样拒绝;事务外的 mapped 写入不误报;事务关闭后作用域不泄漏;reads数组长度 —— 空数组上reads.at(-1)?.transaction也是undefined,只断句柄会因「没发生读」而假绿;error/warn/carve-outdebug全为 0 —— 门对绝大多数部署完全不可见;trxCtx被覆盖;引擎没开过的外来句柄不覆盖(见上)。P2 的钉子按设计翻绿:
engine-transaction-observability.test.ts第 2 节(9 例)钉的正是被删除的那条肢 ——error日志 + 句柄穿透。按三分法它属于「整体替换」而不是「重新拼写」:它的主体没有了。9 例的每一条事实都在新文件里对裁决后的判决重新提问(每事务预算改挂 carve-out —— 拒绝是抛错,抛错从不去重;update/delete;join 嵌套;ScopedContext;事务外不误报;作用域不泄漏),原地留桩只会把同一件事钉两遍。第 1 节(降级 warn-once)原样保留 —— 它仍然纯观测,opts.require只是给了调用方一个选项,没改它覆盖的默认行为。文件头已如实记录这次搬迁。真实输出(合入 main
136308428、全量pnpm build之后重跑;合并前的绿一律作废)四个事务相关测试文件:
五包全跑:
typecheck 与门:
反向验证(三肢,方向先写死再运行)
肢 A —— 去掉同源校验(
buildDriverOptions恢复无条件穿句柄,enforceTransactionOrigin不动)。toEqual({ __trx: 'primary' })时看到的是同一句话,方向相反。匹配:是。肢 B —— 关掉系统写 carve-out,只留拒绝(即「单独先落 #5696 第 2 点」的形状)。
try/catch吃掉,合规行照丢 —— 「响亮化但不修复」不再是论证,是实测到的形状。匹配:是。这是耦合裁决必要性的直接证据。肢 C —— 移除
require判定(引擎面与沙箱面各一处;require由 #6165 引入,在本分支上重跑取证)。owned、降级默认、同源各例保持绿。Tests 5 failed | 28 passed (33),红的正是那 5 条。匹配:是。三肢均已还原,还原后全绿。
必答项
#5226(审计行丢失的症状单)是否应随之关闭?
不需要动 —— 它已于 2026-08-05 由
os-zhuang以completed关闭,早于本批。给 PM 的证据,分两层:objectstack dev --fresh上 sys_audit_log 表不存在 —— 每次元数据写入都以 ERROR + 「Audit write failed」告终,审计轨迹静默丢失 #5226 的可见症状是「元数据写之后成对出现ERROR Insert operation failed {object: sys_audit_log}+WARN Audit write failed」。本 PR 修的正是产生这对日志的那条路径 —— 那些写全部发生在protocol.saveMetaItem的事务内,现在审计写在事务外、在 telemetry 连接上执行,不再no such table。症状被根因修复完全覆盖。objectstack dev --fresh上 sys_audit_log 表不存在 —— 每次元数据写入都以 ERROR + 「Audit write failed」告终,审计轨迹静默丢失 #5226 的前提本身是错的,这一点比覆盖更重要,PM 归档时值得记一笔:它断言「dev --fresh上sys_audit_log表根本没建」,ambient 事务句柄跨数据源泄漏:凡在事务中执行的被审计写入,合规审计行全部静默丢失(#5226 的真实根因) #5351 实测证伪(表建在dev.telemetry.db,50 行在盘上)。它列的两个待查方向双双被排除。它的第二个缺陷(日志级别应为error)已由 PR fix(plugin-audit): 审计行写失败升为error并只报一次 (#5226 之二);#5226 主缺陷前提被证伪 #5350 单独落地。所以objectstack dev --fresh上 sys_audit_log 表不存在 —— 每次元数据写入都以 ERROR + 「Audit write failed」告终,审计轨迹静默丢失 #5226 关得对,只是关它的理由不是它自己写的那个 —— 真正的根因是本单,plugin-audit never provisions sys_audit_log / sys_activity — Setup "System Overview" audit widgets silently render zeros #4887 记录过这个「在另一个 store 里」的误诊形状。结论:无需再动 #5226。 若 PM 要留档,建议在其下补一条指回本 PR 的说明,让下一个搜到该单的人看到根因编号。
#5929 / #5846 是否触其面?
否,与预期一致。两单都在 hook 前置行(pre-image)读取与按对象需求门那一片:#5846 是单 id update 三读同一行 + 全局注册架空按对象门;#5929 是
sys_fetch_previous_delete以object: '*'注册让 delete 侧的门恒真。本 PR 的 diff 完全不碰这片:未改
hasHooksFor、未改wantsPreImage/needsPriorRecord、未改packages/objectql/src/plugin.ts的任何内建 hook 注册、未改 plugin-audit。唯一的接触面是顺序:enforceTransactionOrigin在写路径最顶端、早于前置行读与 hook 派发,所以一次被拒绝的跨源业务写现在不会再触发那些读 —— 这只会让两单描述的多余读更少,不改变它们的判定逻辑,两单的事实陈述与验收判据全部不受影响。#4619(母单)落地后还剩什么
三点全部落完:
opts.requirefail-closed(#6165,已合入 main136308428)error诊断(#5724);收紧 = 本 PR(业务写拒绝 + 系统账本 carve-out + 句柄不跨驱动)owned信号(#6165,已合入)给 PM 的收口判断:本 PR 合入后 #4619 可以关(
completed),关闭说明里指向 #5724 / #6165 / 本 PR 三处,以及 ADR-0067/0119 的 2026-08-06 修订。IDataDriver契约,量级和性质都不同,已独立立单;ScopedContext.transaction不 join ambient 事务(违反 ADR-0067 D2),实施中发现,独立立单。#4619 原文明确排除的跨驱动原子性 / 两阶段提交依然不在任何单里,也不应因本批而被认为「还欠着」—— 它是被裁掉的,不是被推迟的。
范围外发现(PD #10,均未在本 PR 内修改)
finding,未认领)ScopedContext.transaction不 join ambient 事务,沙箱 hook 体里会在事务中再开一个事务(finding,未认领)消费者影响
单数据源部署(绝大多数)零变化:不拒绝、不 carve-out、不打日志 —— 已由回归组钉住。门只在「注册了第二个数据源」+「对象路由到它」+「事务开着」三者同时成立时可达。changeset 里带了升级须知(跨驱动业务写从静默部分提交变为拒绝;审计写从静默丢失变为事务外落盘 + 孤儿行语义)。