@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.
Files changed (76) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +10 -1
  4. package/dist/approval-card.d.ts +47 -0
  5. package/dist/approval-card.js +18 -0
  6. package/dist/boot/parked-revive-gate.d.ts +3 -0
  7. package/dist/boot/parked-revive-gate.js +2 -2
  8. package/dist/boot/resolve-spec.js +1 -1
  9. package/dist/boot/runner-deps.d.ts +2 -2
  10. package/dist/boot/runner-deps.js +4 -0
  11. package/dist/boot/stores.d.ts +1 -0
  12. package/dist/boot/stores.js +7 -4
  13. package/dist/config-catalog.d.ts +0 -29
  14. package/dist/config-catalog.js +3 -0
  15. package/dist/config-types.d.ts +12 -2
  16. package/dist/config.js +13 -2
  17. package/dist/cross-session-settings.d.ts +70 -0
  18. package/dist/cross-session-settings.js +37 -0
  19. package/dist/deployment-governance.d.ts +30 -1
  20. package/dist/deployment-governance.js +11 -5
  21. package/dist/http/routes/approvals-assistant.js +3 -0
  22. package/dist/http/routes/capabilities.js +2 -0
  23. package/dist/http/routes/diagnostics.js +1 -0
  24. package/dist/http/server.d.ts +6 -0
  25. package/dist/main.js +11 -4
  26. package/dist/memory-scope.d.ts +20 -0
  27. package/dist/memory-scope.js +9 -1
  28. package/dist/observability/run-terminal-log.d.ts +4 -0
  29. package/dist/observability/run-terminal-log.js +14 -0
  30. package/dist/plugins/approval-ask-store-sql.d.ts +9 -9
  31. package/dist/plugins/approval-ask-store-sql.js +1 -1
  32. package/dist/plugins/device-store-sql.d.ts +38 -10
  33. package/dist/plugins/device-store-sql.js +28 -9
  34. package/dist/plugins/file-history-store-sql.d.ts +20 -3
  35. package/dist/plugins/file-history-store-sql.js +39 -36
  36. package/dist/plugins/image-bake-store-sql.js +2 -2
  37. package/dist/plugins/mailbox-store-sql.d.ts +5 -2
  38. package/dist/plugins/mailbox-store-sql.js +2 -2
  39. package/dist/plugins/permission-rule-store-file.d.ts +4 -4
  40. package/dist/plugins/permission-rule-store-file.js +4 -4
  41. package/dist/plugins/permission-rule-store-sql.d.ts +15 -11
  42. package/dist/plugins/permission-rule-store-sql.js +7 -11
  43. package/dist/plugins/retention-lane-store-sql.d.ts +10 -6
  44. package/dist/plugins/retention-lane-store-sql.js +1 -1
  45. package/dist/plugins/retention-store-sql.d.ts +11 -15
  46. package/dist/plugins/retention-store-sql.js +6 -2
  47. package/dist/plugins/session-policy-store-sql.d.ts +9 -2
  48. package/dist/plugins/shared-memory-store-sql.d.ts +25 -19
  49. package/dist/plugins/shared-memory-store-sql.js +17 -15
  50. package/dist/plugins/sql-driver.d.ts +78 -26
  51. package/dist/plugins/sql-driver.js +97 -14
  52. package/dist/plugins/store-backend.d.ts +6 -2
  53. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  54. package/dist/plugins/task-list-store-sql.js +1 -1
  55. package/dist/plugins/usage-window-store-sql.d.ts +5 -3
  56. package/dist/plugins/usage-window-store-sql.js +1 -5
  57. package/dist/rules-consent.d.ts +54 -12
  58. package/dist/rules-consent.js +4 -4
  59. package/dist/run-local.d.ts +2 -1
  60. package/dist/run-local.js +3 -3
  61. package/dist/runs.d.ts +13 -0
  62. package/dist/runs.js +2 -0
  63. package/dist/sql-engine-posture.d.ts +49 -0
  64. package/dist/sql-engine-posture.js +13 -0
  65. package/dist/task-settings.d.ts +13 -3
  66. package/dist/task-settings.js +1 -1
  67. package/dist/tool-approval.d.ts +41 -1
  68. package/dist/tool-approval.js +7 -1
  69. package/dist/trace/core-keyset-guard.d.ts +4 -4
  70. package/dist/trace/engine-notice-wire.d.ts +1 -1
  71. package/dist/trace/engine-notice-wire.js +3 -0
  72. package/dist/trace/project.d.ts +25 -0
  73. package/dist/trace/project.js +26 -0
  74. package/dist/trace/sema-provenance.d.ts +7 -1
  75. package/dist/trace/sema-provenance.js +1 -1
  76. 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({
@@ -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 ruleConsent = permissionRuleStores ? createRuleConsentLane(permissionRuleStores) : undefined;
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: permissionRuleStores?.provider,
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(),
@@ -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;
@@ -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 = resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
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 骨架)。本店没有 image-bake
18
- * 那种「NOT EXISTS 快照读」问题(每条 CAS 都是对已存在主键行的 `UPDATE WHERE state=?`,MySQL/TiDB
19
- * 对被更新行本身恒是 current read,不受 REPEATABLE READ 快照影响),所以走普通 `begin()`,不需要
20
- * `beginPessimistic()`。
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 侧本事务显式 `BEGIN PESSIMISTIC`
507
- * (`beginPessimistic()`,不赌部署的 `tidb_txn_mode` 缺省):悲观锁定读是 CURRENT read,读到的是
508
- * 并发赢家**已提交**的最新版本(乐观模式下 FOR UPDATE 读 start_ts 快照,会把赢家终局读旧 ——
509
- * 正是 bindBatch 失败臂注记的那类方言分歧,这里用显式事务模式把它结构性关死);PG 侧
510
- * `beginPessimistic()` = 普通 `BEGIN`,READ COMMITTEDFOR UPDATE 本就等待并返回最新已提交版本。
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.beginPessimistic();
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()`/`beginPessimistic()` → `query()` → `commit()`,失败
22
- * `rollback()`。**失败臂的回读必须走当前这条 `conn`**(approval 店复审 F1 的真缺陷:走池级 query
23
- * 会与自己持有的连接死锁),且**先 rollback 再读**(读到最新已提交视图)。
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) `INSERT IGNORE` 在一个事务里。
134
+ * §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与首绑 CAS 插入在一个事务里。
133
135
  *
134
- * 🔴 `beginPessimistic` + `FOR UPDATE`:设备行必须是**当前读**。快照读下,一次与吊销并发的绑定会
135
- * 读到「还 active」的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(sql-driver.ts 头注点名的
136
- * double-admit 形)。
137
- * 并发首绑:`INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected 是引擎对「谁赢」的权威回答;
138
- * 输家回读既有行 —— 同 deviceId = 幂等成功,异 deviceId = `conflict`(无未定义态)。
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
- * 🔴 `beginPessimistic` + 两次 `FOR UPDATE`(bindSession 的同一条真库红的射程):
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.beginPessimistic();
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.beginPessimistic();
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.beginPessimistic();
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.beginPessimistic();
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 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 (?, ?, ?, ?, NOW(3), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, now(), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject]);
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 immediately after insert");
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.beginPessimistic();
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 `beginPessimistic` and
108
- * BEFORE the scope lock — the window where another replica's publish can commit while this tx's
109
- * snapshot is already pinned (TiDB snapshot-reads plain SELECTs, D2). Production body is empty. */
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>;