@sema-agent/server 7.58.0 → 7.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +10 -1
- package/dist/approval-card.d.ts +47 -0
- package/dist/approval-card.js +18 -0
- package/dist/boot/parked-revive-gate.d.ts +3 -0
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.js +1 -1
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +7 -4
- package/dist/config-catalog.d.ts +0 -29
- package/dist/config-catalog.js +3 -0
- package/dist/config-types.d.ts +12 -2
- package/dist/config.js +13 -2
- package/dist/cross-session-settings.d.ts +70 -0
- package/dist/cross-session-settings.js +37 -0
- package/dist/deployment-governance.d.ts +30 -1
- package/dist/deployment-governance.js +11 -5
- package/dist/http/routes/approvals-assistant.js +3 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/diagnostics.js +1 -0
- package/dist/http/server.d.ts +6 -0
- package/dist/main.js +11 -4
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +9 -1
- package/dist/observability/run-terminal-log.d.ts +4 -0
- package/dist/observability/run-terminal-log.js +14 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +9 -9
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/device-store-sql.d.ts +38 -10
- package/dist/plugins/device-store-sql.js +28 -9
- package/dist/plugins/file-history-store-sql.d.ts +20 -3
- package/dist/plugins/file-history-store-sql.js +39 -36
- package/dist/plugins/image-bake-store-sql.js +2 -2
- package/dist/plugins/mailbox-store-sql.d.ts +5 -2
- package/dist/plugins/mailbox-store-sql.js +2 -2
- package/dist/plugins/permission-rule-store-file.d.ts +4 -4
- package/dist/plugins/permission-rule-store-file.js +4 -4
- package/dist/plugins/permission-rule-store-sql.d.ts +15 -11
- package/dist/plugins/permission-rule-store-sql.js +7 -11
- package/dist/plugins/retention-lane-store-sql.d.ts +10 -6
- package/dist/plugins/retention-lane-store-sql.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +11 -15
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/session-policy-store-sql.d.ts +9 -2
- package/dist/plugins/shared-memory-store-sql.d.ts +25 -19
- package/dist/plugins/shared-memory-store-sql.js +17 -15
- package/dist/plugins/sql-driver.d.ts +78 -26
- package/dist/plugins/sql-driver.js +97 -14
- package/dist/plugins/store-backend.d.ts +6 -2
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/usage-window-store-sql.d.ts +5 -3
- package/dist/plugins/usage-window-store-sql.js +1 -5
- package/dist/rules-consent.d.ts +54 -12
- package/dist/rules-consent.js +4 -4
- package/dist/run-local.d.ts +2 -1
- package/dist/run-local.js +3 -3
- package/dist/runs.d.ts +13 -0
- package/dist/runs.js +2 -0
- package/dist/sql-engine-posture.d.ts +49 -0
- package/dist/sql-engine-posture.js +13 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +41 -1
- package/dist/tool-approval.js +7 -1
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +3 -0
- package/dist/trace/project.d.ts +25 -0
- package/dist/trace/project.js +26 -0
- package/dist/trace/sema-provenance.d.ts +7 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { posix } from "node:path";
|
|
2
|
-
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, err, ok, } from "@sema-agent/core";
|
|
1
|
+
import { join, posix } from "node:path";
|
|
2
|
+
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, ok, } from "@sema-agent/core";
|
|
3
3
|
import { createDurableAskPolicy } from "./approval.js";
|
|
4
4
|
export function createDurableQuestionGate(live) {
|
|
5
5
|
if (live === undefined)
|
|
@@ -47,17 +47,23 @@ export function buildOnlySensitiveBaselineWarning(config, seat) {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
export function createDeploymentGovernanceInputs(config, pathAdjudication) {
|
|
50
|
+
export function createDeploymentGovernanceInputs(config, pathAdjudication, roots) {
|
|
51
51
|
const sensitivePathPolicy = config.sensitiveWritePatterns.length > 0
|
|
52
52
|
? (() => {
|
|
53
|
+
const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
|
|
53
54
|
const realTarget = createSensitivePathPolicy({
|
|
54
55
|
env: pathAdjudication.env,
|
|
55
56
|
patterns: config.sensitiveWritePatterns,
|
|
56
57
|
...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
|
|
58
|
+
...dataRootOpt,
|
|
57
59
|
});
|
|
60
|
+
const transcriptGuard = config.localDataRoot !== undefined
|
|
61
|
+
? createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })
|
|
62
|
+
: undefined;
|
|
63
|
+
const withTranscripts = (p) => (transcriptGuard ? combinePolicies(p, transcriptGuard) : p);
|
|
58
64
|
if (pathAdjudication.cwd !== undefined)
|
|
59
|
-
return realTarget;
|
|
60
|
-
return combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns }));
|
|
65
|
+
return withTranscripts(realTarget);
|
|
66
|
+
return withTranscripts(combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt })));
|
|
61
67
|
})()
|
|
62
68
|
: undefined;
|
|
63
69
|
return {
|
|
@@ -177,6 +177,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
177
177
|
...(s.spentMicroUsd !== undefined ? { spentMicroUsd: s.spentMicroUsd } : {}),
|
|
178
178
|
...(s.deadline !== undefined ? { deadline: s.deadline } : {}),
|
|
179
179
|
...(s.hasBidiControls === true ? { hasBidiControls: true } : {}),
|
|
180
|
+
...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
181
|
+
...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
|
|
182
|
+
...(s.origin !== undefined ? { origin: s.origin } : {}),
|
|
180
183
|
objective: ctx?.body?.objective ?? null,
|
|
181
184
|
input,
|
|
182
185
|
};
|
|
@@ -9,6 +9,7 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
|
9
9
|
import { sendJson, sendError } from "../send.js";
|
|
10
10
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
11
11
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
12
|
+
import { projectSqlEngineCapability } from "../../sql-engine-posture.js";
|
|
12
13
|
import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
|
|
13
14
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
14
15
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
@@ -76,6 +77,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
76
77
|
memory: false,
|
|
77
78
|
memoryWrite: false,
|
|
78
79
|
memoryEngine: projectMemoryEngineCapability(deps.memoryPosture),
|
|
80
|
+
sql: projectSqlEngineCapability(deps.sqlEngineFacts?.()),
|
|
79
81
|
sessionBackgroundable: Boolean(deps.runStore),
|
|
80
82
|
runMemoryCaptureOptOut: Boolean(deps.runStore),
|
|
81
83
|
sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
|
|
@@ -86,6 +86,7 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
|
|
|
86
86
|
sendJson(res, 200, {
|
|
87
87
|
static: deps.staticWiring,
|
|
88
88
|
memoryPosture: deps.memoryPosture ?? null,
|
|
89
|
+
sqlEngine: deps.sqlEngineFacts?.() ?? null,
|
|
89
90
|
configApply: deps.configApplyState?.() ?? null,
|
|
90
91
|
mcpRevocations: deps.mcpRevocationState?.() ?? null,
|
|
91
92
|
serverGates: buildServerWiringGates({
|
package/dist/http/server.d.ts
CHANGED
|
@@ -654,6 +654,12 @@ export interface ServiceDeploymentDeps {
|
|
|
654
654
|
* `GET /v1/capabilities` 的 `memoryEngine` 位(窄投影)。缺席 = 这个进程不是由 composition root 装
|
|
655
655
|
* 起来的(测试夹具形)⇒ 诊断面报 null、能力位取诚实下限 false。 */
|
|
656
656
|
memoryPosture?: import("../memory-posture.js").MemoryPosture;
|
|
657
|
+
/** S-131:SQL 引擎姿态的 **live getter**(`sql-engine-posture.ts` 的单一推导点 —— boot 期借一条池连接
|
|
658
|
+
* 触发驱动的连接初始化,读的是 `SET` 之后**回读复核**过的真值)。两个读面共用:
|
|
659
|
+
* `GET /v1/diagnostics/wiring` 的 `sqlEngine` 段(operator-only,全量含版本)与 `GET /v1/capabilities`
|
|
660
|
+
* 的 `sql` 位(窄投影)。getter 而不是快照:池重连后驱动会重新初始化并刷新事实,快照会说谎。
|
|
661
|
+
* 缺席 / 返回 null = 本部署没有 SQL 后端(env-only worker、local 文件后端)—— 诚实缺席,不铸空壳。 */
|
|
662
|
+
sqlEngineFacts?: () => import("../plugins/sql-driver.js").SqlEngineFacts | null;
|
|
657
663
|
/** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own
|
|
658
664
|
* `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store
|
|
659
665
|
* proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, expandTiers, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
4
|
+
import { Runner, createPermissionRuleStoreProvider, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, expandTiers, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
5
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
6
6
|
import { posIntEnv } from "./session-watch.js";
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
@@ -128,7 +128,7 @@ async function main() {
|
|
|
128
128
|
}
|
|
129
129
|
const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
|
|
130
130
|
await configCenter.applyLocalRemoteExec();
|
|
131
|
-
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
|
|
131
|
+
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, sqlEngineFacts, } = await openStores({ config, logger, metrics, localRoot });
|
|
132
132
|
const instanceId = uuidv7();
|
|
133
133
|
const sessionCaptureRecordStore = backend?.sessionCaptureRecords();
|
|
134
134
|
const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine, { ...(sessionCaptureRecordStore !== undefined ? { captureRecordStore: sessionCaptureRecordStore } : {}) }) : undefined;
|
|
@@ -153,7 +153,12 @@ async function main() {
|
|
|
153
153
|
}
|
|
154
154
|
const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
|
|
155
155
|
const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
|
|
156
|
-
const
|
|
156
|
+
const permissionRuleProvider = permissionRuleStores
|
|
157
|
+
? createPermissionRuleStoreProvider({ durable: permissionRuleStores.durable })
|
|
158
|
+
: undefined;
|
|
159
|
+
const ruleConsent = permissionRuleStores && permissionRuleProvider
|
|
160
|
+
? createRuleConsentLane({ ...permissionRuleStores, provider: permissionRuleProvider })
|
|
161
|
+
: undefined;
|
|
157
162
|
if (config.permissionRulesEnabled && permissionRuleStores === undefined) {
|
|
158
163
|
logger.info("permission_rules_lane_unavailable", {
|
|
159
164
|
knob: "PERMISSION_RULES_ENABLED",
|
|
@@ -268,7 +273,7 @@ async function main() {
|
|
|
268
273
|
sessionCaptureRecordStore,
|
|
269
274
|
memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileHistoryStore,
|
|
270
275
|
legacyRewindBoundaryProbe: backend?.legacyRewindBoundaryProbe ? backend.legacyRewindBoundaryProbe.bind(backend) : undefined,
|
|
271
|
-
permissionRuleStore:
|
|
276
|
+
permissionRuleStore: permissionRuleProvider,
|
|
272
277
|
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
273
278
|
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
|
|
274
279
|
governanceSeams,
|
|
@@ -475,6 +480,7 @@ async function main() {
|
|
|
475
480
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
476
481
|
? createParkedReviveInheritedGate({
|
|
477
482
|
config, question, approvalExemptionStore, logger, localRoot,
|
|
483
|
+
memoryEngineRoot: memoryEngine?.root,
|
|
478
484
|
approverSeat: createRunnerDepsOnAsk(toolApproval),
|
|
479
485
|
...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
|
|
480
486
|
autoModeSeatMounted: runnerDeps.autoMode !== undefined,
|
|
@@ -694,6 +700,7 @@ async function main() {
|
|
|
694
700
|
workflowModelAllowlist: workflowModelAllowlistSnapshot,
|
|
695
701
|
staticWiring,
|
|
696
702
|
memoryPosture,
|
|
703
|
+
sqlEngineFacts,
|
|
697
704
|
instanceId,
|
|
698
705
|
capabilities: {
|
|
699
706
|
version: serviceVersion(),
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -84,6 +84,26 @@ engineWired: boolean): {
|
|
|
84
84
|
* Returns the backend + the resolved config root (the root also rides on `RunnerDeps.memoryEngineDir` so
|
|
85
85
|
* core derives the B3 control plane beside it).
|
|
86
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* S-133(7.59.0 合并重扫 wf_5ac18676 #1)—— **引擎数据根的唯一推导点**。模型是用 fs 工具往这里写记忆的
|
|
89
|
+
* ([ref] 记忆边界不变式,boot/memory-boundary.ts),所以三件事必须读**同一只**值:①boot 起哪只记忆后端的根
|
|
90
|
+
* (boot/stores.ts 三腿)、②`RunnerDeps.memoryEngineDir`、③守卫集 `createSensitivePathPolicy` 的 `dataRoot`
|
|
91
|
+
* (deployment-governance.ts:落在这个根**里面**的目标按相对根判,根自己的祖先段不算目标的账)。
|
|
92
|
+
* 此前 ③ 读的是 `config.localDataRoot`(本地**店**根:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR`),
|
|
93
|
+
* 而 ① 的 file 腿读的是 `MEMORY_ENGINE_DIR ?? AGENT_DATA_DIR`——两条链在「店根另指他处 + 记忆根坐在守卫段下
|
|
94
|
+
* (壳布局 `~/.sema/engine-data`)」时分家 ⇒ 记忆库每一次 Write 被硬拒(7.58.0 放行 ⇒ 回归);反向
|
|
95
|
+
* (`CONFIG_LOCAL_DIR=~/.sema` 当 dataRoot)则让配置根自身免检。两根本就是两件事:店根归誊本门,引擎根归本函数。
|
|
96
|
+
*
|
|
97
|
+
* 形与 boot/stores.ts 逐字同律:DB 记忆腿(pg/tidb)的根 = `<localDataRoot ?? fallbackRoot>/memory-work`
|
|
98
|
+
* (per-worker 物化区);file 腿 = `resolveMemoryEngineRoot(memoryEngineDir ?? fallbackRoot)`。**不判引擎是否
|
|
99
|
+
* 启用**——纯路径推导,禁用态下守卫仍以「若开引擎会写到哪」为根,与 core 缺省(`$AGENT_DATA_DIR`)同向。
|
|
100
|
+
*/
|
|
101
|
+
export declare function memoryEngineRootFor(config: Pick<ServiceConfig, "memoryEngineBackend" | "memoryEngineDir" | "localDataRoot">, fallbackRoot?: string): string;
|
|
102
|
+
/** file 后端的根链(`MEMORY_ENGINE_DIR ?? fallback ?? $AGENT_DATA_DIR ?? ~/.ai-agent`)—— 与后端选择器**无关**:
|
|
103
|
+
* `memoryEngineBackendFor` 恒造 file 后端(run-local 从 env 继承 `MEMORY_ENGINE_BACKEND=pg|tidb` 时仍是 file),
|
|
104
|
+
* 所以它的根必须走这条链而不是 `memoryEngineRootFor` 的分派(codex 修复验证轮 [medium]:分派会把 run-local 的
|
|
105
|
+
* 记忆搬到 `<localDataRoot>/memory-work`,存量记忆与控制面升级后隐形)。 */
|
|
106
|
+
export declare function fileMemoryEngineRoot(config: Pick<ServiceConfig, "memoryEngineDir">, fallbackRoot?: string): string;
|
|
87
107
|
export declare function memoryEngineBackendFor(config: ServiceConfig, fallbackRoot?: string): {
|
|
88
108
|
backend: FileMemoryEngineBackend;
|
|
89
109
|
root: string;
|
package/dist/memory-scope.js
CHANGED
|
@@ -60,12 +60,20 @@ export function buildMemoryRemoteLaneWarn(config, engineWired) {
|
|
|
60
60
|
"WORKER fs; verify the lane really shares it.",
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
export function memoryEngineRootFor(config, fallbackRoot) {
|
|
64
|
+
if (config.memoryEngineBackend !== "file")
|
|
65
|
+
return join(config.localDataRoot ?? fallbackRoot ?? resolveMemoryEngineRoot(undefined), "memory-work");
|
|
66
|
+
return fileMemoryEngineRoot(config, fallbackRoot);
|
|
67
|
+
}
|
|
68
|
+
export function fileMemoryEngineRoot(config, fallbackRoot) {
|
|
69
|
+
return resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
|
|
70
|
+
}
|
|
63
71
|
export function memoryEngineBackendFor(config, fallbackRoot) {
|
|
64
72
|
if (config.requirePrincipal === true || !config.memoryEngineEnabled)
|
|
65
73
|
return undefined;
|
|
66
74
|
if (memoryEngineRemoteLanePosture(config)?.posture === "dark")
|
|
67
75
|
return undefined;
|
|
68
|
-
const root =
|
|
76
|
+
const root = fileMemoryEngineRoot(config, fallbackRoot);
|
|
69
77
|
const dir = join(root, "memory");
|
|
70
78
|
return { backend: new FileMemoryEngineBackend(dir, { controlDir: deriveControlPlaneDir(root, dir) }), root };
|
|
71
79
|
}
|
|
@@ -60,6 +60,10 @@ export declare function redactTerminalResult<T extends {
|
|
|
60
60
|
remoteEnvFailures?: readonly {
|
|
61
61
|
message?: string | undefined;
|
|
62
62
|
}[] | undefined;
|
|
63
|
+
/** 🔴 形上**故意**是 `unknown`:本函数也是**读面**(旧持久 blob / 非 core Runner 的形不可信),
|
|
64
|
+
* 声明成 `{path,edits}[]` 会逼每一个喂坏形的调用点铸一次宽松断言 —— 而「坏形从哪来」正是本座要
|
|
65
|
+
* 处理的事(同一份不信任在 `remoteEnvFailures` 的实现里也已成文)。运行期逐项判形,不抛。 */
|
|
66
|
+
editedFiles?: unknown;
|
|
63
67
|
} | null | undefined>(result: T): T;
|
|
64
68
|
/** S-107(第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行
|
|
65
69
|
* (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。
|
|
@@ -74,6 +74,7 @@ export function redactErrorMessage(raw) {
|
|
|
74
74
|
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
const MAX_EDITED_FILES = 1000;
|
|
77
78
|
export function redactTerminalResult(result) {
|
|
78
79
|
if (result === undefined || result === null)
|
|
79
80
|
return result;
|
|
@@ -94,6 +95,19 @@ export function redactTerminalResult(result) {
|
|
|
94
95
|
: r.remoteEnvFailures;
|
|
95
96
|
changed = true;
|
|
96
97
|
}
|
|
98
|
+
if (r.editedFiles !== undefined) {
|
|
99
|
+
out.editedFiles = Array.isArray(r.editedFiles)
|
|
100
|
+
? r.editedFiles
|
|
101
|
+
.slice(0, MAX_EDITED_FILES)
|
|
102
|
+
.map((f) => {
|
|
103
|
+
if (f === null || typeof f !== "object")
|
|
104
|
+
return f;
|
|
105
|
+
const path = f.path;
|
|
106
|
+
return typeof path === "string" ? { ...f, path: redactSecrets(path) } : f;
|
|
107
|
+
})
|
|
108
|
+
: r.editedFiles;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
97
111
|
return changed ? out : result;
|
|
98
112
|
}
|
|
99
113
|
export function redactLedgerEventData(type, data) {
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* (`rev = rev + 1`),这样只要「行在且谓词命中」就必然 affected=1,不会被这个驱动怪癖
|
|
15
15
|
* 误判成「CAS 输了」。本文件每一条 CAS UPDATE 都带 `rev = rev + 1`。
|
|
16
16
|
* 3. 事务:走 `SqlDriver.connect()` → `conn.begin()` → 一串 `conn.query()` → `conn.commit()`,失败
|
|
17
|
-
* `conn.rollback()`(形照 image-bake-store-sql.ts 的 try/catch/finally 骨架)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* `conn.rollback()`(形照 image-bake-store-sql.ts 的 try/catch/finally 骨架)。事务动词只有一个;
|
|
18
|
+
* 读语义(隔离级 / TiDB 悲观模式 / 事务内集合读必须锁读)的判据只有一处:{@link SqlTxConn.begin}
|
|
19
|
+
* 的 `@contract txn.read-semantics`。本店每条 CAS 都是对已存在主键行的 `UPDATE … WHERE state=?`
|
|
20
|
+
* (DML 对被更新行恒是 current read,与快照无关),败方读数则一律 `SELECT … FOR UPDATE`。
|
|
21
21
|
*
|
|
22
22
|
* ── 方言差异(显式写在每个调用点,[ref] A12 doctrine)──────────────────────────────────────────
|
|
23
23
|
* - `?` 占位符(位置序)vs `$n`(显式编号)——动态列清单(`transitionAsk`/`resolveProvisional`,列集合
|
|
@@ -503,11 +503,11 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
|
|
|
503
503
|
* 取消臂的 STREAM_PENDING→VOID(此前走通用 `transitionAsk`,无败方读数)。
|
|
504
504
|
*
|
|
505
505
|
* **原子性证明(两方言各一句,红先钉在 db-integration 的 [ref] 组)**:
|
|
506
|
-
* · 败方读数是**同一事务内**的 `SELECT … FOR UPDATE` —— TiDB
|
|
507
|
-
* (
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* `
|
|
506
|
+
* · 败方读数是**同一事务内**的 `SELECT … FOR UPDATE` —— TiDB 侧的悲观锁定读是 CURRENT read,读到的是
|
|
507
|
+
* 并发赢家**已提交**的最新版本(乐观模式下 FOR UPDATE 读 start_ts 快照,会把赢家终局读旧 —— 正是
|
|
508
|
+
* bindBatch 失败臂注记的那类方言分歧)。悲观模式**不赌部署的 `tidb_txn_mode` 缺省**:它是连接初始化
|
|
509
|
+
* 的结构保证,设不上就拒启({@link SqlTxConn.begin} 的 @contract txn.read-semantics)。PG 侧
|
|
510
|
+
* `BEGIN` = READ COMMITTED,FOR UPDATE 本就等待并返回最新已提交版本。
|
|
511
511
|
* · CAS 输(affected=0)⇒ 行在 UPDATE 那一刻已非 STREAM_PENDING(或不在/错 scope),而非 pending 态
|
|
512
512
|
* 在状态机上无回边 ⇒ 锁定读读到的恒 ≥ 线性化点,且 DECIDED 决议不可变 —— 答案自足。
|
|
513
513
|
* · 本事务在输臂上除批行 rev 自增外零写,rollback 无痕。
|
|
@@ -440,7 +440,7 @@ export class SqlApprovalAskStore {
|
|
|
440
440
|
const dialect = this.db.dialect;
|
|
441
441
|
const conn = await this.db.connect();
|
|
442
442
|
try {
|
|
443
|
-
await conn.
|
|
443
|
+
await conn.begin();
|
|
444
444
|
if (intent === "expire") {
|
|
445
445
|
const lockRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=?`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=$1`), [batchId]);
|
|
446
446
|
if (lockRes.affected !== 1) {
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
* ——**所有** CAS UPDATE 因此必须带一个恒变列(`rev = rev + 1`)。本文件每一条 CAS UPDATE 都带。
|
|
19
19
|
* 在本店这条尤其致命:心跳续租在「同一毫秒内重复心跳」时 SET 的值可能与当前值相同,没有 rev
|
|
20
20
|
* 就会被判成「本连接已失权」⇒ 设备被自己的心跳踢下线。
|
|
21
|
-
* 3. 事务:`SqlDriver.connect()` → `begin()
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* 3. 事务:`SqlDriver.connect()` → `begin()` → `query()` → `commit()`,失败 `rollback()`。读语义
|
|
22
|
+
* (隔离级 / TiDB 悲观模式 / 事务内集合读必须锁读)的判据只有一处:`sql-driver.ts` 头注的
|
|
23
|
+
* {@link SqlTxConn.begin} `@contract txn.read-semantics`。**失败臂的回读必须走当前这条 `conn`**
|
|
24
|
+
* (approval 店复审 F1 的真缺陷:走池级 query 会与自己持有的连接死锁),且**先 rollback 再读**
|
|
25
|
+
* (回滚后本连接回到 autocommit,那条 SELECT 自成事务 ⇒ 读到最新已提交视图)。
|
|
24
26
|
*
|
|
25
27
|
* ── 方言差异(A12 doctrine:每处显式写在调用点)────────────────────────────────────────────────────
|
|
26
28
|
* - `?`(位置序) vs `$n`(显式编号);动态 WHERE(`listAudit`)靠 `ph(dialect, n)` 生成。
|
|
@@ -129,20 +131,46 @@ export declare class SqlDeviceStore implements DeviceStore {
|
|
|
129
131
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
130
132
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
131
133
|
/**
|
|
132
|
-
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)
|
|
134
|
+
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与首绑 CAS 插入在一个事务里。
|
|
133
135
|
*
|
|
134
|
-
* 🔴 `
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
136
|
+
* 🔴 `FOR UPDATE`(设备行):设备行必须是**当前读**。普通读下,一次与吊销并发的绑定会读到「还 active」
|
|
137
|
+
* 的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(double-admit 形)。锁读在两引擎都是当前读 ——
|
|
138
|
+
* 前提写在 {@link SqlTxConn.begin} 的 @contract txn.read-semantics。这把锁在 `devices` 表上,与下面
|
|
139
|
+
* 被插入的 `device_session` 行不是同一行。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 形 B · cas-insert(S-127 / B-013)—— 本站点是全仓 `INSERT IGNORE` 的**正当用法**:
|
|
142
|
+
* `INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected(1/0)是引擎对「谁赢」的**权威**回答,而
|
|
143
|
+
* `ON DUPLICATE KEY UPDATE pk = pk` 的 affected 新行/既有行都是 1、分不出赢家 ⇒ 这里换不得动词。
|
|
144
|
+
* 代价必须付清:**同一事务此后不得对被插入的那一行做加锁读**。InnoDB 下撞重复键的 IGNORE 在既有行
|
|
145
|
+
* 取 **S 锁**,随后的 `SELECT … FOR UPDATE` 要升 X;两个并发首绑各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK
|
|
146
|
+
* (TiDB 悲观事务无共享锁,这一形在它上面恒绿 —— 双库门看不见)。于是两条路径分家:
|
|
147
|
+
* · **赢家**(affected === 1):行是自己刚插的,回执用**入参 + 插入时刻**直接构造,零回读。插入时刻
|
|
148
|
+
* 仍由 **DB 时钟**给出(插入前一句 `SELECT NOW(3)` / `SELECT now()`,再把它当参数写进 INSERT)——
|
|
149
|
+
* 时间权威留在 DB,回执里的 `boundAtMs` 与落库值**逐字节同一个**,不是应用侧的近似。
|
|
150
|
+
* · **输家**(affected === 0):**结束当前事务**,在**新事务**里 `SELECT … FOR UPDATE` 观察。那一刻
|
|
151
|
+
* 赢家已提交,读到的是一把干净的 X 锁(无升级);判 同 deviceId = 幂等成功,异 deviceId =
|
|
152
|
+
* `conflict`(无未定义态)。设备状态门在输家路径上已经过完,不需要重来。
|
|
139
153
|
*/
|
|
140
154
|
bindSession(input: BindSessionInput): Promise<BindSessionResult>;
|
|
155
|
+
/**
|
|
156
|
+
* 首绑 CAS 的**输家观察**(形 B 的后半,S-127)—— 独立事务,与那次 `INSERT IGNORE` 无锁关系。
|
|
157
|
+
*
|
|
158
|
+
* 🔴 `FOR UPDATE`(**真库红修**,2026-08-28 双库跑当场抓到,mock 抓不到):TiDB 的悲观事务里**普通
|
|
159
|
+
* SELECT 仍是快照读**(读的是本事务 start_ts 的视图),只有加锁读才是当前读。放在新事务里读同样要
|
|
160
|
+
* 这把锁 —— 新事务的 start_ts 虽在赢家提交之后,但「读的是不是当前」这条判据不该靠时序巧合来满足。
|
|
161
|
+
* 与旧形的**唯一**差别:此刻我们手上没有那一行的 S 锁,于是这把 X 是**直接取**的,不是升级 ⇒ 两个
|
|
162
|
+
* 并发输家不会互等(B-013 的死锁形正是那次升级)。
|
|
163
|
+
*
|
|
164
|
+
* 行不在 = 赢家的绑定在这一瞬被 `unbindSession` 删了(真实可能,不是不变量破)。这仍然是**响亮**的:
|
|
165
|
+
* 首绑既没成、也说不出冲突对象,折成任何一种业务回执都是撒谎([ref]:未知失败不伪装成正常拒绝)。
|
|
166
|
+
*/
|
|
167
|
+
private observeLostBind;
|
|
141
168
|
/**
|
|
142
169
|
* O4 显式换绑(2026-08-30 v2 稿 §2)—— **单事务**:目标设备门(当前读)+ 幂等短路 + 前态 CAS +
|
|
143
170
|
* `session_rebound` 审计行一起提交。三形语义 oracle = `InMemoryDeviceStore.rebindSession`。
|
|
144
171
|
*
|
|
145
|
-
* 🔴
|
|
172
|
+
* 🔴 两次 `FOR UPDATE`(bindSession 的同一条真库红的射程;当前读的前提见
|
|
173
|
+
* {@link SqlTxConn.begin} 的 @contract txn.read-semantics):
|
|
146
174
|
* · 设备行必须当前读 —— 快照读会把「与吊销并发的换绑」绑到一台刚被吊销的设备上(double-admit 形);
|
|
147
175
|
* · 绑定行必须当前读且**持锁到提交** —— 两个并发换绑(同 expectedRev)在锁上串行:赢家 rev+1 提交,
|
|
148
176
|
* 输家的锁读看到新 rev ⇒ 走 `rev_conflict` 携当前行(单赢家,无未定义态)。锁读判等之后,CAS
|
|
@@ -367,7 +367,7 @@ export class SqlDeviceStore {
|
|
|
367
367
|
}
|
|
368
368
|
const conn = await this.db.connect();
|
|
369
369
|
try {
|
|
370
|
-
await conn.
|
|
370
|
+
await conn.begin();
|
|
371
371
|
const cas = await conn.query(this.q(`UPDATE ${DEVICE_ENROLL_TOKENS_TABLE} SET consumed_at = NOW(3), consumed_device_id = ?, consumed_pubkey = ?, rev = rev + 1
|
|
372
372
|
WHERE token_hash = ? AND consumed_at IS NULL AND expires_at > NOW(3) AND target_tenant = ? AND target_subject = ?`, `UPDATE ${DEVICE_ENROLL_TOKENS_TABLE} SET consumed_at = now(), consumed_device_id = $1, consumed_pubkey = $2, rev = rev + 1
|
|
373
373
|
WHERE token_hash = $3 AND consumed_at IS NULL AND expires_at > now() AND target_tenant = $4 AND target_subject = $5`), [input.device.deviceId, input.device.pubkey, input.tokenHash, input.requester.tenant, input.requester.subject]);
|
|
@@ -454,7 +454,7 @@ export class SqlDeviceStore {
|
|
|
454
454
|
assertRevokeShape(input);
|
|
455
455
|
const conn = await this.db.connect();
|
|
456
456
|
try {
|
|
457
|
-
await conn.
|
|
457
|
+
await conn.begin();
|
|
458
458
|
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET status = 'revoked', revoked_at = NOW(3), revoked_by = ?, revoke_reason = ?, lease_expires_at = NULL, conn_generation = conn_generation + 1, rev = rev + 1
|
|
459
459
|
WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? AND status = 'active'`, `UPDATE ${DEVICES_TABLE} SET status = 'revoked', revoked_at = now(), revoked_by = $1, revoke_reason = $2, lease_expires_at = NULL, conn_generation = conn_generation + 1, rev = rev + 1
|
|
460
460
|
WHERE device_id = $3 AND owner_tenant = $4 AND owner_subject = $5 AND status = 'active'`), [input.revokedBy, input.reason ?? null, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
@@ -484,7 +484,7 @@ export class SqlDeviceStore {
|
|
|
484
484
|
assertAcquireShape(input);
|
|
485
485
|
const conn = await this.db.connect();
|
|
486
486
|
try {
|
|
487
|
-
await conn.
|
|
487
|
+
await conn.begin();
|
|
488
488
|
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = ?, replica_id = ?, lease_expires_at = ${nowPlusSeconds("tidb", "?")}, connected_at = NOW(3), last_seen_at = NOW(3), rev = rev + 1
|
|
489
489
|
WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= NOW(3))`, `UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = $1, replica_id = $2, lease_expires_at = ${nowPlusSeconds("pg", "$3")}, connected_at = now(), last_seen_at = now(), rev = rev + 1
|
|
490
490
|
WHERE device_id = $4 AND owner_tenant = $5 AND owner_subject = $6 AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= now())`), [input.connEpoch, input.replicaId, input.leaseSeconds, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
@@ -523,7 +523,7 @@ export class SqlDeviceStore {
|
|
|
523
523
|
assertDeviceOwnerShape(input.owner);
|
|
524
524
|
const conn = await this.db.connect();
|
|
525
525
|
try {
|
|
526
|
-
await conn.
|
|
526
|
+
await conn.begin();
|
|
527
527
|
const dev = await conn.query(this.q(`SELECT status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
528
528
|
if (!dev.rows[0]) {
|
|
529
529
|
await conn.rollback();
|
|
@@ -533,14 +533,33 @@ export class SqlDeviceStore {
|
|
|
533
533
|
await conn.rollback();
|
|
534
534
|
return { outcome: "device_revoked" };
|
|
535
535
|
}
|
|
536
|
-
const
|
|
536
|
+
const clock = await conn.query(this.q(`SELECT ${ms("tidb", NOW("tidb"), "bound_at_ms")}`, `SELECT ${ms("pg", NOW("pg"), "bound_at_ms")}`));
|
|
537
|
+
const boundAtMs = Number(clock.rows[0].bound_at_ms);
|
|
538
|
+
const ins = await conn.query(this.q(`INSERT IGNORE INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES (?, ?, ?, ?, FROM_UNIXTIME(? / 1000), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, to_timestamp($5::double precision / 1000.0), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject, boundAtMs]);
|
|
539
|
+
if (ins.affected === 1) {
|
|
540
|
+
await conn.commit();
|
|
541
|
+
return { outcome: "bound", row: { rootSessionId: input.rootSessionId, deviceId: input.deviceId, owner: input.owner, boundAtMs, rev: 0 } };
|
|
542
|
+
}
|
|
543
|
+
await conn.rollback();
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
await conn.rollback().catch(() => undefined);
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
conn.release();
|
|
551
|
+
}
|
|
552
|
+
return await this.observeLostBind(input);
|
|
553
|
+
}
|
|
554
|
+
async observeLostBind(input) {
|
|
555
|
+
const conn = await this.db.connect();
|
|
556
|
+
try {
|
|
557
|
+
await conn.begin();
|
|
537
558
|
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ? FOR UPDATE`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1 FOR UPDATE`), [input.rootSessionId]);
|
|
538
559
|
await conn.commit();
|
|
539
560
|
const row = rows[0] ? mapSessionRow(rows[0]) : null;
|
|
540
561
|
if (!row)
|
|
541
|
-
throw new Error("device-store: session binding vanished
|
|
542
|
-
if (ins.affected === 1)
|
|
543
|
-
return { outcome: "bound", row };
|
|
562
|
+
throw new Error("device-store: the winning session binding vanished before the losing binder could observe it");
|
|
544
563
|
const same = row.deviceId === input.deviceId && row.owner.tenant === input.owner.tenant && row.owner.subject === input.owner.subject;
|
|
545
564
|
return { outcome: same ? "idempotent" : "conflict", row };
|
|
546
565
|
}
|
|
@@ -556,7 +575,7 @@ export class SqlDeviceStore {
|
|
|
556
575
|
assertRebindShape(input);
|
|
557
576
|
const conn = await this.db.connect();
|
|
558
577
|
try {
|
|
559
|
-
await conn.
|
|
578
|
+
await conn.begin();
|
|
560
579
|
const dev = await conn.query(this.q(`SELECT status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [input.toDeviceId, input.owner.tenant, input.owner.subject]);
|
|
561
580
|
if (!dev.rows[0] || parseDeviceStatus(bufToStr(dev.rows[0].status)) !== "active") {
|
|
562
581
|
await conn.rollback();
|
|
@@ -61,6 +61,11 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
61
61
|
* row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
|
|
62
62
|
* longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
|
|
63
63
|
* the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
|
|
64
|
+
*
|
|
65
|
+
* Shape A (S-127): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
|
|
66
|
+
* On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
|
|
67
|
+
* below has to upgrade it to X — two concurrent scope writers each holding an S deadlock (B-013). ON
|
|
68
|
+
* DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
|
|
64
69
|
*/
|
|
65
70
|
private lockScope;
|
|
66
71
|
/** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
|
|
@@ -74,7 +79,11 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
74
79
|
env: ExecutionEnv;
|
|
75
80
|
signal?: AbortSignal;
|
|
76
81
|
}): Promise<FileHistoryResult>;
|
|
82
|
+
/** `forUpdate` = the {@link SqlTxConn.begin} `@contract txn.read-semantics` ③ arm: a collection read that
|
|
83
|
+
* runs inside a transaction must be a LOCKING read (a plain SELECT is pinned to the transaction's start on
|
|
84
|
+
* TiDB, so the lock-wait window's commits are invisible — B-022). Pool-level callers pass nothing. */
|
|
77
85
|
private loadTracked;
|
|
86
|
+
/** `forUpdate` — see {@link loadTracked}: inside a transaction this is a collection read, so it locks. */
|
|
78
87
|
private loadVersions;
|
|
79
88
|
/** [ref] twin 保留条款:这个 entry 的键是否**已花掉**(被 reap 剪过)。台账 `file_history_reaped_entry`。
|
|
80
89
|
* `forUpdate`(r3-①连带):发布 tx 内的复核必须是**锁定读**——TiDB 悲观事务的普通 SELECT 按文档读
|
|
@@ -104,14 +113,18 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
104
113
|
/** PROTECTED RACE SEAM (test-only override): runs inside `restore` between the boundary-header check and
|
|
105
114
|
* the mapping read. Production body is empty. */
|
|
106
115
|
protected duringRestorePlan(_scope: string, _entryId: string): Promise<void>;
|
|
107
|
-
/** PROTECTED RACE SEAM (test-only override): runs inside the publish tx AFTER `
|
|
108
|
-
*
|
|
109
|
-
*
|
|
116
|
+
/** PROTECTED RACE SEAM (test-only override): runs inside the publish tx AFTER `begin()` and BEFORE
|
|
117
|
+
* the scope lock — the window where another replica's publish can commit while this tx's snapshot is
|
|
118
|
+
* already pinned. That window is exactly what B-022 rode: a plain SELECT taken after the lock is still
|
|
119
|
+
* pinned to the transaction's start, so this tx's collection reads are LOCKING reads
|
|
120
|
+
* ({@link SqlTxConn.begin} → `@contract txn.read-semantics`). Production body is empty. */
|
|
110
121
|
protected beforePublishLock(_scope: string): Promise<void>;
|
|
111
122
|
/** PROTECTED RACE SEAM (test-only override): runs inside `exportOf` between the boundary-header read and
|
|
112
123
|
* the mapping read. Production body is empty. */
|
|
113
124
|
protected duringExportGraph(_scope: string): Promise<void>;
|
|
114
125
|
private applyBoundaryRetention;
|
|
126
|
+
/** `forUpdate` — see {@link loadTracked}: the in-transaction re-check must be a LOCKING read, or it just
|
|
127
|
+
* replays the pool-level pre-check's snapshot and its documented `null` arm becomes unreachable. */
|
|
115
128
|
private boundaryExists;
|
|
116
129
|
snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryResult>;
|
|
117
130
|
restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryRestoreResult>;
|
|
@@ -141,6 +154,10 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
141
154
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
142
155
|
adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
|
|
143
156
|
private scopeEmpty;
|
|
157
|
+
/** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are
|
|
158
|
+
* one graph, and a graph materialized from a start-of-transaction snapshot disagrees with the LOCKING
|
|
159
|
+
* emptiness probe that decided to materialize it (that disagreement answered a legal idempotent
|
|
160
|
+
* re-import as `conflict` — B-022). */
|
|
144
161
|
private exportOf;
|
|
145
162
|
exportHistory(scope: string): Promise<FileHistoryExport | null>;
|
|
146
163
|
importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
|