/** * MailboxStore SQL twins([ref] S3c server pg 半场,[ref] 提货单①)——core teammate 信箱 * (SendMessage 链)的 TiDB/PG 双方言实现。挂载缝 = S3c 行为车的 `RunnerDeps.mailboxStore`(core * 命名预告,[ref]§三);本文件先就绪 store 半场,行为车到货接线。 * * 语义真源 = core `InMemoryMailboxStore`(dist/core/mailbox-store.js)+ [ref] 定谳(core 1.372 * 终版文本,conformance 无两可): * 1. **同主重 claim = 续期 + 纳新**(crash-retry 正形):活 lease 只挡**别的** owner;同 owner 任意 * 时刻重 claim 重设 TTL 并拿到当前全部未 ack 消息(maxSeq 随最新行走)。 * 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop` 删整盒(含 box 行) * = 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)。⚠️ **`reap` 自 [ref](core * 2026-07-25)起不再与 drop 同句**:reap 是收件人活着时的龄扫,**清箱保高水位**(删消息、清租约、盒行与 * next_seq 留着)——本文件 2026-09-03(车BF,codex r1 [high])才跟上;此前 reap 删盒行 ⇒ 下一条 append 从 1 * 重铸,陈旧 ack 可删掉一条没人见过的新消息。跨进程试剂盒明钉此条。 * 3. **ack 无 owner 守卫**(信任 claim 赢者链):`seq <= upToSeq` 删行;lease 的 `maxSeq <= upToSeq` * 时顺带清 lease(全量 ack = 释放)。 * 4. **lease 过期判 = 严格 `expiresAt > now` 才挡**;`releaseLease` 只在 owner 匹配时清。 * 5. `peekCount` 含已 lease 消息(可见性 ≠ lease 态);空盒 claim = null(box 行在而消息 0 也 null)。 * 6. **reap = SQL 全量真扫**(core File 实现只扫已加载盒 = 进程内 backstop,[ref] 确认 SQL 是全量 * 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs` **清箱**(删消息 + 清租约, * 盒行/高水位保留,见定谳 2 的 [ref] 半句);空盒(有 box 行无消息)不清(InMemory 同形——`newest === * undefined` 不动)。 * * 键列 = 字节等价(TiDB VARBINARY / PG COLLATE "C",roster F2 案):scope/handle/lease_owner 的 * `=` 必须纯字节——PAD SPACE 近撞不得穿隔离/抢别人 lease。PG unstorable bytes([ref] 三层定谳): * `content` 是消息**内容面**(收件人将读到、注入对话)→ 与 PG 转录 payload 同族 lossy(pgSanitizeText, * U+FFFD 标记,内容不失行);**身份键 scope/handle/owner/from 拒绝式**(pgHasUnstorable → * PgUnstorableError fail-loud——attribution/路由键清洗形变 = 行与查询键错位,协议数据同向)。TiDB * 侧 LONGTEXT/VARBINARY verbatim(replay 面定谳)。 * * 并发正确性(InMemory 单进程无此象限、SQL 多副本有;发车复审 F2/F3/F4 批): * - **锁序统一「盒行 → 消息行」**(append/claim/ack/drop/reap 五面同序)——ack/drop/reap 原为 * 消息先行,与 claim 的盒先行构成 ABBA 死锁形;统一后同盒并发只在盒行锁上排队。 * - drop/reap 先删/锁**盒行**再动消息:反序留「消息已删、盒行未删」窗,并发 append 完整穿过后 * 盒重建会撞存活的孤儿消息行(PK 冲突→此后 append 永久失败)。 * - reap 候删 = 事务外粗筛 + 事务内**逐盒锁定重验**(当前读 newest):窗内新到的消息救活盒, * 绝不连带删信;盒行已被并发 drop = 跳过不计。 * - append 的 ensure 与 FOR UPDATE 之间被并发 drop 提交 = 同事务内重建一次(语义 = drop 后重建, * seq 从 1)。建盒动词 = 形 A(S-127)`ON DUPLICATE KEY UPDATE = `:重复键取 X 锁,与随后的 * `FOR UPDATE` **同锁模式**(`INSERT IGNORE` 取 S 再升 X = InnoDB 上的并发死锁形,B-013)。 * - 键长 > 190 字节写入即拒(fail-loud):截断键与全长查询参数字节比对恒 miss(分配读空行崩/隔离键 * 错位)。守卫在**语句之前**,与建行动词无关 —— 换成 ON DUPLICATE 之后超长键由引擎直接报错(严格 * 模式下 IGNORE 会把它降级成静默截断),方向=更响亮,守卫依旧是第一道。 */ import type { Pool as MySqlPool } from "mysql2/promise"; import type { Pool as PgPool } from "pg"; import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core"; import { type IndexSpec } from "./ensure-index.js"; import type { MailboxRecipientLifecycle, MailboxRecipientTombstone } from "./mailbox-recipient-lifecycle.js"; export declare const MAILBOX_TABLE = "mailbox"; export declare const MAILBOX_MSG_TABLE = "mailbox_message"; /** [ref] 收件人墓碑表(预删除态的**唯一**真源;形与理由见 `mailbox-recipient-lifecycle.ts` 模块头)。 * 🔴 为什么是**新表**而不是给 `mailbox` 加两列:加列走的是本仓「删库重建、不开 ALTER seam」那条政策 * (存量库首跑必红);而 `CREATE TABLE IF NOT EXISTS` 一张新表在存量库上零破坏、零运维动作。语义上也更干净 * —— `mailbox` 行是**盒**事实(next_seq / lease),墓碑是**收件人**事实,两者的生命周期本来就不同 * (`drop` 同时终结两者 —— 墓碑随箱亡,理由全文见 `mailbox-recipient-lifecycle.ts` 模块头)。 */ export declare const MAILBOX_TOMBSTONE_TABLE = "mailbox_tombstone"; /** 店侧日志座(可选;boot 注入 `Logger`)。7.58.0 合并重扫 [medium]:坏 `peer_meta` 行的 fail-closed 抛必须**响亮**——core 唯一的 * claimLease 调用方(peer-session-drain)吞掉该错,店不记这一笔就成了「静默永久卡箱」。抛之前记 `mailbox_peer_meta_corrupt` * (scope/handle/seq/code),运维按 seq 修行。 */ export interface MailboxStoreLogger { warn(event: string, fields: Record): void; } export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise; /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgMailboxSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */ export declare const MAILBOX_INDEXES: readonly IndexSpec[]; export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise): Promise; export declare class TiDBMailboxStore implements MailboxStore, MailboxRecipientLifecycle { private readonly pool; private readonly logger?; /** [ref]([ref] §1.2⑥):跨进程安全**显式**声明 —— core 的 `mailboxCrossProcessMountVerdict` 只对字面 `true` 放行 * peer lane(缺席/false/畸形一律响亮拒挂 `config.peer_lane_unmounted`)。真值前提 = 本文件头注「并发正确性」段:seq * 在事务内经盒行 FOR UPDATE 单铸、lease/ack/drop/reap 统一盒行锁序、reap 事务内逐盒当前读重验 —— 多副本(=多 OS * 进程)共享一个盒本就是 SQL 双生的设计象限。**终验** = core 跨进程试剂盒 `mailboxCrossProcessContract` 跑在真第二个 * OS 进程上:`test/mailbox-cross-process-kit.test.ts`(file 腿恒跑验 harness;tidb/pg 腿 ENV-GATED,发车前双库门必跑, * 红即摘本声明——core 契约逐字「declares true AFTER passing」,声明不许先于试剂盒长期悬空)。 */ readonly crossProcessSafe = true; constructor(pool: MySqlPool, logger?: MailboxStoreLogger | undefined); private tx; append(scope: string, handle: string, msg: MailboxAppendMessage): Promise; claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise; ack(scope: string, handle: string, owner: string, upToSeq: number): Promise; releaseLease(scope: string, handle: string, owner: string): Promise; peekCount(scope: string, handle: string): Promise; drop(scope: string, handle: string): Promise; reap(scope: string, now: number, opts?: { maxAgeMs?: number; }): Promise; /** 见 {@link MailboxRecipientLifecycle.retireRecipient}。**只打标**:不删消息、不动租约、不碰 next_seq。 * 锁序 = 盒行先(与 append / claim / ack / drop / reap 五面统一 —— ABBA 死锁形不许在这里重开), * 于是 retire 与并发 append 在同一把行锁上串行:要么 append 先拿到 seq、要么它读到墓碑被拒,没有中间态。 */ retireRecipient(scope: string, handle: string, at: number, label?: string): Promise; /** 见 {@link MailboxRecipientLifecycle.listRetiredRecipients}。前缀查带 `ESCAPE`(句柄是开集串, * `_`/`%` 若被当通配会把**别的**收件人一起列出来 —— 与本仓 `?q=` 面同一条 escapeLike 纪律)。 */ listRetiredRecipients(scope: string, handlePrefix: string): Promise; } export declare class PgMailboxStore implements MailboxStore, MailboxRecipientLifecycle { private readonly pool; private readonly logger?; /** [ref] 跨进程安全显式声明(TiDB 孪生同注:事务 + 盒行 FOR UPDATE 锁序是真值前提;终验 = test/mailbox-cross-process-kit.test.ts pg 腿)。 */ readonly crossProcessSafe = true; constructor(pool: PgPool, logger?: MailboxStoreLogger | undefined); private tx; /** 身份键拒绝式([ref] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */ private assertIdentity; append(scope: string, handle: string, msg: MailboxAppendMessage): Promise; claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise; ack(scope: string, handle: string, owner: string, upToSeq: number): Promise; releaseLease(scope: string, handle: string, owner: string): Promise; peekCount(scope: string, handle: string): Promise; drop(scope: string, handle: string): Promise; reap(scope: string, now: number, opts?: { maxAgeMs?: number; }): Promise; retireRecipient(scope: string, handle: string, at: number, label?: string): Promise; listRetiredRecipients(scope: string, handlePrefix: string): Promise; } //# sourceMappingURL=mailbox-store-sql.d.ts.map