UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

100 lines (99 loc) • 7.31 kB
import { type MailboxStore } from "../mailbox-store.js"; import { type ContractAssertionRunner } from "./contract-harness.js"; /** * design/159 S1 — the cross-backend {@link MailboxStore} contract, extracted VERBATIM from * `test/mailbox-contract.ts` (design/157 A11's two-layer form) into a vitest-free kit (see * `contract-harness.ts` for the runner seam and `contract-kit-version.ts` for version lockstep). * * 为什么这个家族有契约(结构性复发机制,两次实证):契约 harness 此前只喂 InMemory + File 两个 * 后端,于是没进 harness 的那个后端反复独立漂移 —— * - RB-86:`stores/cc/mailbox-store.ts` 注释自陈 "the contract both bundled backends already * honor, missing only here"(空箱 claimLease 不返回 null,还装上幽灵租约); * - MB1:CC 后端的 `ack` 根本不查 lease 表,注释自陈 "worse than the other two * backends' pre-fix bug"。 * 两次漏的都是同一个后端。修法不是再补一次点,而是把契约本身变成全后端共跑的资产 —— design/159 * 再进一步:第三方后端也拿得到同一份资产。 * * 分层(与 test/mailbox-contract.ts 的挂载对应): * - T1 核心不变量层 = {@link mailboxStoreContract} + {@link mailboxAckOwnershipContract}:每个后端 * 都必须过。任何后端在这一层变红 = 该后端违约,**不许通过放宽契约让它变绿**。核心层一律用 * `scope: "default"`({@link MAILBOX_CONTRACT_SCOPE}),这样租户轴受限的后端(CC inbox 文件) * 也跑同一段代码。 * - T2 可选能力层 = {@link mailboxBundledOnlyContract}:多租户隔离 + 键注入性,只挂具备租户轴/ * 自有键空间的后端(core 的两个捆绑后端;CC 按 D-3 边界豁免,豁免明细在 * `src/stores/cc/mailbox-store.ts` 模块头)。 * * `make()` must return a FRESH, empty store each call. Stores a contract case creates are closed * (`close?.()`) when the case ends — the file backend's append fd is refcounted, so a harness that * runs many cases would otherwise hold descriptors until process exit. */ /** 核心层统一用的 scope —— 每个后端都必须服务它(租户轴受限的后端只服务它)。 */ export declare const MAILBOX_CONTRACT_SCOPE = "default"; /** * T1 核心不变量层 —— 每个后端都跑。用例出处见每条内联注释(`src/core/mailbox-store.ts` 的接口 * 契约段是权威文本)。 */ export declare function mailboxStoreContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>; /** * T1 核心不变量层(MB1 分册)—— ack 的 owner 围栏,与 {@link mailboxStoreContract} 同层同权重, * 单独成函数只是为了让 MB1 的出处叙述留在它自己的挂载文件里 * (`test/mb1-mailbox-ack-ownership.test.ts`)。 */ export declare function mailboxAckOwnershipContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>; /** * T2 可选能力层 —— 只挂具备租户轴 + 自有键空间的后端(core 的两个捆绑后端)。CC 后端按其形态 * 明文豁免(豁免明细在 `src/stores/cc/mailbox-store.ts` 模块头,D-3 裁定),不是「没做到所以 * 放宽」:核心层没有一条为了任何后端而弱化。第三方后端若有租户轴,应当也挂这一层。 */ export declare function mailboxBundledOnlyContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>; export interface MailboxCrossProcessContractHooks { /** Open ONE fresh, empty storage location TWICE: `local` in this process and `peer` served by ANOTHER * OS process over the same storage (the peer is a real second process — an in-process second * instance proves nothing here). `dispose` closes both and may remove the storage. The kit calls it * once per case. */ openPair: () => Promise<{ local: MailboxStore; peer: MailboxStore; dispose: () => Promise<void>; }>; runAssertion?: ContractAssertionRunner; } /** * T2 可选能力层(cross-process 分册,design/385 §1.2⑥ / G11)—— 声明 `crossProcessSafe: true` 的后端必须 * 过的全套判据。钉的是**整套**(跨进程 seq 单铸 + lease/ack 不交叉 + 一方的 housekeeping 不丢另一方的 * 已落盘 append + reap/drop 的 seq 语义跨进程成立),不是「有锁」:一把锁配一份陈旧缓存照样在锁内铸 * 重号、照样把别人的消息压缩掉——所以每一条都从**对方进程的视角**回读。 * * 受众:文件后端(本仓捆绑)、SQL 后端(事务天然过)。进程内 Map 的参照实现与 CC inbox 适配器不声明, * 也不挂——它们不是「没做到所以放宽」,而是根本不在跨进程车道上(挂载判据 `mailboxCrossProcessMountVerdict` * 对未声明的后端响亮拒)。 */ export declare function mailboxCrossProcessContract(hooks: MailboxCrossProcessContractHooks): Promise<void>; export interface MailboxTombstonedRecipientContractHooks { /** Put `(scope, handle)` into the deployment's PRE-DELETE state — whatever that is for this * backend (a cascade marking the session row, a `deleting` column, a tombstone table). Core does * not name the mechanism, only what `append` must then do. */ tombstone: (store: MailboxStore, scope: string, handle: string) => Promise<void>; runAssertion?: ContractAssertionRunner; } /** * T2 可选能力层(pre-delete 分册)—— 只挂**看得见收件人生命周期**的后端:它的 mailbox 行与某个 * 删除级联要清掉的 session 行同生共死(server 的留存删除级联是首个此形消费方)。三个捆绑后端都 * 没有这个视角,因此都不挂——**这不是「做不到所以放宽」**:没有预删除态的后端根本没有可拒的对象, * 而有这个态却静默接受的后端,拿到的 seq 是一张级联马上就要撕掉的回执(`append` 是 durable-first, * 回执即承诺),且没有任何其它面会说出这件事。 * * 契约不规定态怎么置(那是部署自己的删除协议),只钉「拒」这件事的三条轴 —— **三条一起列在这里, * 是因为逐条补的过程本身证明了单看一条会假绿**(每加一条,都有一个能过掉前面全部用例的错误实现被 * 抓出来): * 1. **拒因轴**:必须拒,且每条路径上都带同一个码(空箱与非空箱都算路径); * 2. **副作用轴**:被拒的 append 对箱零改动 —— 不入箱(幽灵投递)、不清箱(删除窗口内静默丢)、 * 也不动活租约(动了就撬开 X-1 的单消费者围栏,同一批消息会被二次投递); * 3. **收件人边界轴**:态属于一个 `(scope, handle)` 整体,且这个整体是**单射**的 —— 兄弟 handle * 不受牵连、跨租户同名不受牵连、拼键歧义不得把两个收件人折成一个。 * 新增用例请按这三条轴归位;要加第四条轴,先说清它是哪一类假绿。 * * 受众前提与 {@link mailboxBundledOnlyContract} 同线:后端有自己的租户轴(生命周期可见的后端就是 * 服务端形态的 store),边界轴的两条用例要用两个 scope 构造。 */ export declare function mailboxTombstonedRecipientContract(mk: () => MailboxStore, hooks: MailboxTombstonedRecipientContractHooks): Promise<void>;