@sema-agent/server 7.57.0 → 7.59.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 (61) hide show
  1. package/USAGE.md +2 -0
  2. package/dist/approval-card.d.ts +47 -0
  3. package/dist/approval-card.js +18 -0
  4. package/dist/boot/parked-revive-gate.d.ts +3 -0
  5. package/dist/boot/parked-revive-gate.js +2 -2
  6. package/dist/boot/resolve-spec.js +1 -1
  7. package/dist/boot/runner-deps.d.ts +2 -2
  8. package/dist/boot/runner-deps.js +4 -0
  9. package/dist/boot/stores.js +15 -8
  10. package/dist/config-catalog.d.ts +0 -29
  11. package/dist/config-catalog.js +3 -0
  12. package/dist/config-center/http-client.js +7 -11
  13. package/dist/config-center/read-warnings.d.ts +20 -0
  14. package/dist/config-center/read-warnings.js +36 -0
  15. package/dist/config-provider.d.ts +3 -0
  16. package/dist/config-provider.js +4 -8
  17. package/dist/config-types.d.ts +12 -2
  18. package/dist/config.js +13 -2
  19. package/dist/cross-session-settings.d.ts +70 -0
  20. package/dist/cross-session-settings.js +37 -0
  21. package/dist/deployment-governance.d.ts +30 -1
  22. package/dist/deployment-governance.js +11 -5
  23. package/dist/hooks/hook-runner.js +8 -8
  24. package/dist/main.js +1 -0
  25. package/dist/memory-scope.d.ts +20 -0
  26. package/dist/memory-scope.js +9 -1
  27. package/dist/observability/metrics.js +1 -1
  28. package/dist/observability/run-terminal-log.d.ts +4 -0
  29. package/dist/observability/run-terminal-log.js +15 -1
  30. package/dist/plugins/device-store-sql.d.ts +30 -6
  31. package/dist/plugins/device-store-sql.js +23 -4
  32. package/dist/plugins/file-history-store-sql.d.ts +5 -0
  33. package/dist/plugins/file-history-store-sql.js +7 -7
  34. package/dist/plugins/image-bake-store-sql.js +1 -1
  35. package/dist/plugins/mailbox-store-sql.d.ts +33 -11
  36. package/dist/plugins/mailbox-store-sql.js +74 -12
  37. package/dist/plugins/retention-lane-store-sql.d.ts +8 -4
  38. package/dist/plugins/retention-lane-store-sql.js +1 -1
  39. package/dist/plugins/retention-store-sql.d.ts +7 -3
  40. package/dist/plugins/retention-store-sql.js +2 -2
  41. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  42. package/dist/plugins/task-list-store-sql.js +1 -1
  43. package/dist/plugins/usage-window-store-sql.d.ts +5 -3
  44. package/dist/plugins/usage-window-store-sql.js +1 -5
  45. package/dist/run-local.d.ts +2 -1
  46. package/dist/run-local.js +3 -3
  47. package/dist/runs.d.ts +13 -0
  48. package/dist/runs.js +2 -0
  49. package/dist/task-settings.d.ts +13 -3
  50. package/dist/task-settings.js +1 -1
  51. package/dist/tool-approval.d.ts +23 -1
  52. package/dist/tool-approval.js +6 -1
  53. package/dist/trace/core-keyset-guard.d.ts +7 -8
  54. package/dist/trace/engine-notice-wire.d.ts +1 -1
  55. package/dist/trace/engine-notice-wire.js +5 -0
  56. package/dist/trace/project.d.ts +45 -3
  57. package/dist/trace/project.js +23 -0
  58. package/dist/trace/redact.js +7 -3
  59. package/dist/trace/sema-provenance.d.ts +15 -0
  60. package/dist/trace/sema-provenance.js +3 -0
  61. package/package.json +3 -3
@@ -0,0 +1,37 @@
1
+ export const CROSS_SESSION_INBOUND_LAYER_NAMES = ["managed", "user", "repo"];
2
+ export const CROSS_SESSION_INBOUND_LAYER_SOURCES = {
3
+ managed: {
4
+ source: "CROSS_SESSION_INBOUND —— 部署/组织层旋钮(词表 accept|hold|refuse,未设=本层不设)",
5
+ wired: true,
6
+ },
7
+ user: {
8
+ source: "body.settings.crossSessionInbound —— 提交面明拒(unsupportedKeys 400,宽容层不读),因引擎座是部署级、无会话上下文," +
9
+ "把某一次提交的值塞进部署座会让一个会话的设置治理另一个会话;候上游每任务座或本仓 per-run 上下文全腿覆盖",
10
+ wired: false,
11
+ },
12
+ repo: {
13
+ source: "本服务没有项目层 settings 源(无 .claude/settings.json 之类的仓库层读点);不造一个来凑数",
14
+ wired: false,
15
+ },
16
+ };
17
+ export function buildCrossSessionInboundLayers(cfg) {
18
+ const layers = {};
19
+ for (const name of CROSS_SESSION_INBOUND_LAYER_NAMES) {
20
+ if (!CROSS_SESSION_INBOUND_LAYER_SOURCES[name].wired)
21
+ continue;
22
+ switch (name) {
23
+ case "managed":
24
+ if (cfg.crossSessionInbound !== undefined)
25
+ layers.managed = cfg.crossSessionInbound;
26
+ break;
27
+ case "user":
28
+ case "repo":
29
+ throw new Error(`cross-session inbound layer "${name}" is marked wired in CROSS_SESSION_INBOUND_LAYER_SOURCES but has no value arm here — wire the source or set wired:false (an unwired layer must be ABSENT, never a silently empty one)`);
30
+ }
31
+ }
32
+ return layers;
33
+ }
34
+ export function crossSessionDialogExpiryOf(cfg) {
35
+ return cfg.crossSessionDialogExpiry;
36
+ }
37
+ //# sourceMappingURL=cross-session-settings.js.map
@@ -11,6 +11,35 @@ export interface DeploymentGovernanceConfigView {
11
11
  readonly commandPolicy?: ServiceConfig["commandPolicy"];
12
12
  readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
13
13
  readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
14
+ /**
15
+ * 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
16
+ * **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
17
+ * {@link DeploymentGovernanceRoots});下面这段历史注保留,记的是 S-125 件⑦ 把两根混作一根的由来。
18
+ *
19
+ * S-125 件⑦(core 7.4.0 [ref] HRD-PRM-10)—— 当时把本键原样交给 `createSensitivePathPolicy` 的 `dataRoot`。
20
+ *
21
+ * 为什么必须**显式**传而不是吃 core 的缺省:core 的缺省是 `$AGENT_DATA_DIR ?? ~/.ai-agent`,而本仓的
22
+ * 数据根解析链多一节最高优先级的覆盖(`config.ts`:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR
23
+ * → ~/.ai-agent`)。只设 `LOCAL_DATA_ROOT=/data/sema` 的部署,数据真的躺在 `/data/sema`,而 core 的缺省
24
+ * 会去猜 `~/.ai-agent` ⇒ 守卫把真数据根**当普通路径**判(它的祖先段一旦命中守卫集里的某一条,整棵子树
25
+ * 连同模型被**指示**要写的记忆库一起被硬拒),同时又把一个不存在的目录当成豁免区。两个方向都错,且都静默。
26
+ * ⇒ 本键 = 「这道门与 transcript 门说的是同一个目录」的单一属主,值与 `ServiceConfig.localDataRoot`
27
+ * **同一个**解析结果(不在本口重算,重算就是第二个写者)。
28
+ *
29
+ * 缺席(窄接口的老调用方 / 测试夹具)⇒ **不铸 `dataRoot` 键**,core 走它自己的缺省 —— 与本口既有的
30
+ * `rootPath` 条件展开同姿势:缺席是「本调用方没有这项知识」,不是「数据根是 undefined」。
31
+ */
32
+ readonly localDataRoot?: ServiceConfig["localDataRoot"];
33
+ }
34
+ /**
35
+ * S-133 —— 守卫集 `dataRoot` 的**显式供给**(与 `DeploymentGovernanceConfigView.localDataRoot` 是两件事):
36
+ * `engineDataRoot` = 引擎写记忆的根 = boot 已铸的 `memoryEngine.root`(boot/stores.ts 是唯一写者;推导函数
37
+ * `memoryEngineRootFor` 只在那里与 `memoryEngineBackendFor` 里调)。**调用腿读那只活对象,不重推导**——重推导
38
+ * 就是第二个写者(7.59.0 重扫 codex 轮抓到 HTTP 腿的 fallback 与 stores 不同源,正是这一形)。参数非可选:
39
+ * 一条腿忘了 = 编译红。`undefined` = 引擎未接(或窄接口的测试夹具),那时不铸 `dataRoot` 键,core 走自己的缺省。
40
+ */
41
+ export interface DeploymentGovernanceRoots {
42
+ readonly engineDataRoot: string | undefined;
14
43
  }
15
44
  /** 审批基线读的配置切面(同上,窄接口)。 */
16
45
  export interface ApprovalBaselineConfigView {
@@ -147,7 +176,7 @@ export declare function buildOnlySensitiveBaselineWarning(config: Pick<Deploymen
147
176
  * `manualModeShellGate`/`sensitivePathPolicy` 按在场性条件展开 —— `applyRuntimeGovernance` 的
148
177
  * `!== undefined` 判据对两者等价,但 profile 是折叠面的可观测字节,搬家不许顺手改。
149
178
  */
150
- export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication): DeploymentGovernanceInputs;
179
+ export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication, roots: DeploymentGovernanceRoots): DeploymentGovernanceInputs;
151
180
  /**
152
181
  * 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座([ref] 件一)。**恒非
153
182
  * `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
@@ -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 {
@@ -673,7 +673,7 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
673
673
  if (!out)
674
674
  continue;
675
675
  if (typeof out.systemMessage === "string")
676
- ctx.logger.warn("hook_system_message", { event, message: clip(out.systemMessage, 500) });
676
+ ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
677
677
  if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
678
678
  ctx.logger.warn("hook_result_unsupported", { event });
679
679
  }
@@ -747,7 +747,7 @@ export function createTaskHooks(config, ctx) {
747
747
  if (!out)
748
748
  continue;
749
749
  if (typeof out.systemMessage === "string")
750
- ctx.logger.warn("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
750
+ ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
751
751
  if (out.continue === false) {
752
752
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
753
753
  return { action: "deny", message: reason, ...buildHookContextField(contexts) };
@@ -845,7 +845,7 @@ export function createTaskHooks(config, ctx) {
845
845
  if (!out)
846
846
  continue;
847
847
  if (typeof out.systemMessage === "string")
848
- ctx.logger.warn("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
848
+ ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
849
849
  if (out.continue === false) {
850
850
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
851
851
  }
@@ -895,7 +895,7 @@ export function createTaskHooks(config, ctx) {
895
895
  if (!out)
896
896
  continue;
897
897
  if (typeof out.systemMessage === "string")
898
- ctx.logger.warn("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
898
+ ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
899
899
  if (out.continue === false)
900
900
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
901
901
  if (out.decision === "block" && typeof out.reason === "string")
@@ -942,7 +942,7 @@ export function createTaskHooks(config, ctx) {
942
942
  if (!out)
943
943
  continue;
944
944
  if (typeof out.systemMessage === "string")
945
- ctx.logger.warn("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
945
+ ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
946
946
  if (out.continue === false)
947
947
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
948
948
  if (out.decision === "block" && typeof out.reason === "string")
@@ -974,7 +974,7 @@ export function createTaskHooks(config, ctx) {
974
974
  if (!out)
975
975
  continue;
976
976
  if (typeof out.systemMessage === "string")
977
- ctx.logger.warn("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
977
+ ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
978
978
  if (out.continue === false) {
979
979
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
980
980
  return { block: reason, ...buildHookContextField(contexts) };
@@ -1063,7 +1063,7 @@ export function createTaskHooks(config, ctx) {
1063
1063
  continue;
1064
1064
  }
1065
1065
  if (typeof out.systemMessage === "string")
1066
- ctx.logger.warn("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1066
+ ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1067
1067
  if (out.continue === false)
1068
1068
  sawContinueFalse = true;
1069
1069
  if (out.decision === "block" && block === undefined) {
@@ -1122,7 +1122,7 @@ export function createTaskHooks(config, ctx) {
1122
1122
  if (!out)
1123
1123
  continue;
1124
1124
  if (typeof out.systemMessage === "string")
1125
- ctx.logger.warn("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1125
+ ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1126
1126
  if (out.continue === false) {
1127
1127
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1128
1128
  }
package/dist/main.js CHANGED
@@ -475,6 +475,7 @@ async function main() {
475
475
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
476
476
  ? createParkedReviveInheritedGate({
477
477
  config, question, approvalExemptionStore, logger, localRoot,
478
+ memoryEngineRoot: memoryEngine?.root,
478
479
  approverSeat: createRunnerDepsOnAsk(toolApproval),
479
480
  ...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
480
481
  autoModeSeatMounted: runnerDeps.autoMode !== undefined,
@@ -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
  }
@@ -235,7 +235,7 @@ export function createMetrics() {
235
235
  m.counter("remote_env_resume_total", "E2B remote-env resumeVM outcomes (v1.5, provider/result)");
236
236
  m.counter("remote_env_exec_stream_read_timeout_total", "E2B execStream idle read-timeouts (#1128, provider)");
237
237
  m.counter("resume_anchor_capture_failed", "E18 resume-at per-turn anchor captures that failed (best-effort, run unaffected)");
238
- m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open)");
238
+ m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open/waiting_first_token/recovered/gave_up)");
239
239
  m.gauge("store_backend_degraded", "1 when the auto DB probe failed at boot and this replica degraded to in-memory (S5)");
240
240
  m.counter("images_omitted_total", "Tasks whose images will be degraded to text placeholders (no-vision model), by model (S6)");
241
241
  m.counter("runs_reaped_total", "Run rows flipped by the background reapers (S7), by kind (stale/suspended/expired_checkpoint)");
@@ -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) {
@@ -177,7 +191,7 @@ export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlin
177
191
  function diffScanRefusal(patch, direct) {
178
192
  if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
179
193
  return "combined-diff";
180
- const fence = /-{4,5} ?BEGIN[^\n-]*PRIVATE KEY(?: BLOCK)?[^\n-]*-{4,5}/;
194
+ const fence = /-{4,5} ?BEGIN(?:[^\n-]|-(?!-))*PRIVATE KEY(?: BLOCK)?(?:[^\n-]|-(?!-))*-{4,5}/;
181
195
  if (fence.test(direct))
182
196
  return "pem-fence-survived";
183
197
  const ppkField = /Private-(?:MAC|Hash): [0-9a-fA-F]{32}/;
@@ -129,15 +129,39 @@ export declare class SqlDeviceStore implements DeviceStore {
129
129
  heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
130
130
  releaseConnection(deviceId: string, generation: number): Promise<boolean>;
131
131
  /**
132
- * §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner) `INSERT IGNORE` 在一个事务里。
132
+ * §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与首绑 CAS 插入在一个事务里。
133
133
  *
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`(无未定义态)。
134
+ * 🔴 `beginPessimistic` + `FOR UPDATE`(设备行):设备行必须是**当前读**。快照读下,一次与吊销并发的
135
+ * 绑定会读到「还 active」的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(sql-driver.ts 头注点名
136
+ * double-admit 形)。这把锁在 `devices` 表上,与下面被插入的 `device_session` 行不是同一行。
137
+ *
138
+ * 🔴 B · cas-insert(S-127 / B-013)—— 本站点是全仓 `INSERT IGNORE` 的**正当用法**:
139
+ * `INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected(1/0)是引擎对「谁赢」的**权威**回答,而
140
+ * `ON DUPLICATE KEY UPDATE pk = pk` 的 affected 新行/既有行都是 1、分不出赢家 ⇒ 这里换不得动词。
141
+ * 代价必须付清:**同一事务此后不得对被插入的那一行做加锁读**。InnoDB 下撞重复键的 IGNORE 在既有行
142
+ * 取 **S 锁**,随后的 `SELECT … FOR UPDATE` 要升 X;两个并发首绑各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK
143
+ * (TiDB 悲观事务无共享锁,这一形在它上面恒绿 —— 双库门看不见)。于是两条路径分家:
144
+ * · **赢家**(affected === 1):行是自己刚插的,回执用**入参 + 插入时刻**直接构造,零回读。插入时刻
145
+ * 仍由 **DB 时钟**给出(插入前一句 `SELECT NOW(3)` / `SELECT now()`,再把它当参数写进 INSERT)——
146
+ * 时间权威留在 DB,回执里的 `boundAtMs` 与落库值**逐字节同一个**,不是应用侧的近似。
147
+ * · **输家**(affected === 0):**结束当前事务**,在**新事务**里 `SELECT … FOR UPDATE` 观察。那一刻
148
+ * 赢家已提交,读到的是一把干净的 X 锁(无升级);判 同 deviceId = 幂等成功,异 deviceId =
149
+ * `conflict`(无未定义态)。设备状态门在输家路径上已经过完,不需要重来。
139
150
  */
140
151
  bindSession(input: BindSessionInput): Promise<BindSessionResult>;
152
+ /**
153
+ * 首绑 CAS 的**输家观察**(形 B 的后半,S-127)—— 独立事务,与那次 `INSERT IGNORE` 无锁关系。
154
+ *
155
+ * 🔴 `FOR UPDATE`(**真库红修**,2026-08-28 双库跑当场抓到,mock 抓不到):TiDB 的悲观事务里**普通
156
+ * SELECT 仍是快照读**(读的是本事务 start_ts 的视图),只有加锁读才是当前读。放在新事务里读同样要
157
+ * 这把锁 —— 新事务的 start_ts 虽在赢家提交之后,但「读的是不是当前」这条判据不该靠时序巧合来满足。
158
+ * 与旧形的**唯一**差别:此刻我们手上没有那一行的 S 锁,于是这把 X 是**直接取**的,不是升级 ⇒ 两个
159
+ * 并发输家不会互等(B-013 的死锁形正是那次升级)。
160
+ *
161
+ * 行不在 = 赢家的绑定在这一瞬被 `unbindSession` 删了(真实可能,不是不变量破)。这仍然是**响亮**的:
162
+ * 首绑既没成、也说不出冲突对象,折成任何一种业务回执都是撒谎([ref]:未知失败不伪装成正常拒绝)。
163
+ */
164
+ private observeLostBind;
141
165
  /**
142
166
  * O4 显式换绑(2026-08-30 v2 稿 §2)—— **单事务**:目标设备门(当前读)+ 幂等短路 + 前态 CAS +
143
167
  * `session_rebound` 审计行一起提交。三形语义 oracle = `InMemoryDeviceStore.rebindSession`。
@@ -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.beginPessimistic();
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
  }
@@ -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;
@@ -49,7 +49,7 @@ export class SqlFileHistoryStore {
49
49
  for (const scope of [...new Set(scopes)].sort()) {
50
50
  let locked = false;
51
51
  for (let i = 0; i < 3 && !locked; i++) {
52
- await conn.query(this.q("INSERT IGNORE INTO file_history_scope (scope, created_at_ms) VALUES (?,?)", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
52
+ await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
53
53
  const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
54
54
  locked = Boolean(r.rows[0]);
55
55
  }
@@ -358,15 +358,15 @@ export class SqlFileHistoryStore {
358
358
  }
359
359
  const seqRow = await conn.query(this.q("SELECT publish_seq FROM file_history_boundary WHERE scope = ? ORDER BY publish_seq DESC LIMIT 1 FOR UPDATE", "SELECT publish_seq FROM file_history_boundary WHERE scope = $1 ORDER BY publish_seq DESC LIMIT 1"), [scope]);
360
360
  const publishSeq = (seqRow.rows[0] !== undefined ? Number(seqRow.rows[0].publish_seq) : 0) + 1;
361
- const ins = await conn.query(this.q("INSERT IGNORE INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES (?,?,CAST(UNIX_TIMESTAMP(NOW(3))*1000 AS SIGNED),?)", "INSERT INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES ($1,$2,(EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint,$3) ON CONFLICT (scope, entry_id) DO NOTHING"), [scope, entryId, publishSeq]);
362
- if (ins.affected !== 1) {
363
- const won = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
364
- if (!won.rows[0]) {
365
- throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
366
- }
361
+ const priorWinner = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
362
+ if (priorWinner.rows[0]) {
367
363
  await conn.rollback();
368
364
  return { ok: true };
369
365
  }
366
+ const ins = await conn.query(this.q("INSERT IGNORE INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES (?,?,CAST(UNIX_TIMESTAMP(NOW(3))*1000 AS SIGNED),?)", "INSERT INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES ($1,$2,(EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint,$3) ON CONFLICT (scope, entry_id) DO NOTHING"), [scope, entryId, publishSeq]);
367
+ if (ins.affected !== 1) {
368
+ throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
369
+ }
370
370
  for (const [keyHash, cap] of staged) {
371
371
  let version;
372
372
  if (cap.kind === "unchanged") {
@@ -140,7 +140,7 @@ export class SqlImageBake {
140
140
  try {
141
141
  if (this.db.dialect === "tidb") {
142
142
  await conn.beginPessimistic();
143
- await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES (?)", [this.poolName]);
143
+ await conn.query("INSERT INTO image_bake_admit (pool) VALUES (?) ON DUPLICATE KEY UPDATE pool = pool", [this.poolName]);
144
144
  await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
145
145
  const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
146
146
  if (busy.rows.length === 0) {
@@ -7,15 +7,19 @@
7
7
  * 终版文本,conformance 无两可):
8
8
  * 1. **同主重 claim = 续期 + 纳新**(crash-retry 正形):活 lease 只挡**别的** owner;同 owner 任意
9
9
  * 时刻重 claim 重设 TTL 并拿到当前全部未 ack 消息(maxSeq 随最新行走)。
10
- * 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop`/`reap` 删整盒
11
- * (含 box 行)= 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)
10
+ * 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop` 删整盒(含 box 行)
11
+ * = 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)。⚠️ **`reap` 自 [ref](core
12
+ * 2026-07-25)起不再与 drop 同句**:reap 是收件人活着时的龄扫,**清箱保高水位**(删消息、清租约、盒行与
13
+ * next_seq 留着)——本文件 2026-09-03(车BF,codex r1 [high])才跟上;此前 reap 删盒行 ⇒ 下一条 append 从 1
14
+ * 重铸,陈旧 ack 可删掉一条没人见过的新消息。跨进程试剂盒明钉此条。
12
15
  * 3. **ack 无 owner 守卫**(信任 claim 赢者链):`seq <= upToSeq` 删行;lease 的 `maxSeq <= upToSeq`
13
16
  * 时顺带清 lease(全量 ack = 释放)。
14
17
  * 4. **lease 过期判 = 严格 `expiresAt > now` 才挡**;`releaseLease` 只在 owner 匹配时清。
15
18
  * 5. `peekCount` 含已 lease 消息(可见性 ≠ lease 态);空盒 claim = null(box 行在而消息 0 也 null)。
16
19
  * 6. **reap = SQL 全量真扫**(core File 实现只扫已加载盒 = 进程内 backstop,[ref] 确认 SQL 是全量
17
- * 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs` 删整盒;空盒(
18
- * box 行无消息)不删(InMemory 同形——`newest === undefined` 不删)。
20
+ * 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs` **清箱**(删消息 + 清租约,
21
+ * 盒行/高水位保留,见定谳 2 的 [ref] 半句);空盒(有 box 行无消息)不清(InMemory 同形——`newest ===
22
+ * undefined` 不动)。
19
23
  *
20
24
  * 键列 = 字节等价(TiDB VARBINARY / PG COLLATE "C",roster F2 案):scope/handle/lease_owner 的
21
25
  * `=` 必须纯字节——PAD SPACE 近撞不得穿隔离/抢别人 lease。PG unstorable bytes([ref] 三层定谳):
@@ -32,20 +36,36 @@
32
36
  * - reap 候删 = 事务外粗筛 + 事务内**逐盒锁定重验**(当前读 newest):窗内新到的消息救活盒,
33
37
  * 绝不连带删信;盒行已被并发 drop = 跳过不计。
34
38
  * - append 的 ensure 与 FOR UPDATE 之间被并发 drop 提交 = 同事务内重建一次(语义 = drop 后重建,
35
- * seq 从 1)。键长 > 190 字节写入即拒(fail-loud)——TiDB `INSERT IGNORE` 会把超长键**静默截断**,
36
- * 截断键与全长查询参数字节比对恒 miss(分配读空行崩/隔离键错位)。
39
+ * seq 从 1)。建盒动词 = A(S-127)`ON DUPLICATE KEY UPDATE <pk> = <pk>`:重复键取 X 锁,与随后的
40
+ * `FOR UPDATE` **同锁模式**(`INSERT IGNORE` 取 S 再升 X = InnoDB 上的并发死锁形,B-013)。
41
+ * - 键长 > 190 字节写入即拒(fail-loud):截断键与全长查询参数字节比对恒 miss(分配读空行崩/隔离键
42
+ * 错位)。守卫在**语句之前**,与建行动词无关 —— 换成 ON DUPLICATE 之后超长键由引擎直接报错(严格
43
+ * 模式下 IGNORE 会把它降级成静默截断),方向=更响亮,守卫依旧是第一道。
37
44
  */
38
45
  import type { Pool as MySqlPool } from "mysql2/promise";
39
46
  import type { Pool as PgPool } from "pg";
40
- import type { MailboxLease, MailboxStore } from "@sema-agent/core";
41
- type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
47
+ import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
42
48
  export declare const MAILBOX_TABLE = "mailbox";
43
49
  export declare const MAILBOX_MSG_TABLE = "mailbox_message";
50
+ /** 店侧日志座(可选;boot 注入 `Logger`)。7.58.0 合并重扫 [medium]:坏 `peer_meta` 行的 fail-closed 抛必须**响亮**——core 唯一的
51
+ * claimLease 调用方(peer-session-drain)吞掉该错,店不记这一笔就成了「静默永久卡箱」。抛之前记 `mailbox_peer_meta_corrupt`
52
+ * (scope/handle/seq/code),运维按 seq 修行。 */
53
+ export interface MailboxStoreLogger {
54
+ warn(event: string, fields: Record<string, unknown>): void;
55
+ }
44
56
  export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
45
57
  export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
46
58
  export declare class TiDBMailboxStore implements MailboxStore {
47
59
  private readonly pool;
48
- constructor(pool: MySqlPool);
60
+ private readonly logger?;
61
+ /** [ref]([ref] §1.2⑥):跨进程安全**显式**声明 —— core 的 `mailboxCrossProcessMountVerdict` 只对字面 `true` 放行
62
+ * peer lane(缺席/false/畸形一律响亮拒挂 `config.peer_lane_unmounted`)。真值前提 = 本文件头注「并发正确性」段:seq
63
+ * 在事务内经盒行 FOR UPDATE 单铸、lease/ack/drop/reap 统一盒行锁序、reap 事务内逐盒当前读重验 —— 多副本(=多 OS
64
+ * 进程)共享一个盒本就是 SQL 双生的设计象限。**终验** = core 跨进程试剂盒 `mailboxCrossProcessContract` 跑在真第二个
65
+ * OS 进程上:`test/mailbox-cross-process-kit.test.ts`(file 腿恒跑验 harness;tidb/pg 腿 ENV-GATED,发车前双库门必跑,
66
+ * 红即摘本声明——core 契约逐字「declares true AFTER passing」,声明不许先于试剂盒长期悬空)。 */
67
+ readonly crossProcessSafe = true;
68
+ constructor(pool: MySqlPool, logger?: MailboxStoreLogger | undefined);
49
69
  private tx;
50
70
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
51
71
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
@@ -59,7 +79,10 @@ export declare class TiDBMailboxStore implements MailboxStore {
59
79
  }
60
80
  export declare class PgMailboxStore implements MailboxStore {
61
81
  private readonly pool;
62
- constructor(pool: PgPool);
82
+ private readonly logger?;
83
+ /** [ref] 跨进程安全显式声明(TiDB 孪生同注:事务 + 盒行 FOR UPDATE 锁序是真值前提;终验 = test/mailbox-cross-process-kit.test.ts pg 腿)。 */
84
+ readonly crossProcessSafe = true;
85
+ constructor(pool: PgPool, logger?: MailboxStoreLogger | undefined);
63
86
  private tx;
64
87
  /** 身份键拒绝式([ref] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
65
88
  private assertIdentity;
@@ -73,5 +96,4 @@ export declare class PgMailboxStore implements MailboxStore {
73
96
  maxAgeMs?: number;
74
97
  }): Promise<number>;
75
98
  }
76
- export {};
77
99
  //# sourceMappingURL=mailbox-store-sql.d.ts.map