/** * [ref] C18(core 5.48.0 [ref]「不造新店」提货批,黑板 [ref] 点名义务)——**subagent 转录持久化** * 的 placement 语义,SQL 孪生共用的一份判据。 * * ## 这是什么 * * core 5.48.0 给 `SessionStore` 开了一组**全 optional** 的 placement 面:一只**声明** `placements` * (「本店懂 placement 语义,且落在 subagent 分区里的会话是这个耐久度」)+ `acquire(id,{placement})` * 的创建形 + `listPlaced` / `placementOf` 两只读面。声明即承诺**六条义务**(core `SessionStore.placements` * 顶注逐字): * * 1. placed 会话享受所声明的耐久度(重启后 `acquire(id,{requireExisting:true})` 仍到得了); * 2. placed 会话**不上宿主枚举面**(壳的 /resume 列表不许把子代转录当成可续聊会话); * 3. **未** placed 的会话,acquire 语义逐字节不变; * 4. `release(placedId)` 是**真删除**(durable 史一并),联合 reap 的「行与会话同亡」靠它; * 5. placement **first-write 不可变**:re-open(`requireExisting`,带不带 placement 参数)返回同一 * 会话且**永不重写**元组; * 6. placed id **不可被 claim**:后来的 claim 形 acquire(无 `requireExisting`、无创建 placement) * 撞上已存在的 placed id ⇒ 响亮拒 `SessionError("placement_refused")`,绝不附着。 * * ## 为什么判据住在这里而不是各写一份 * * 本仓成文教训(`memory-origin-law.ts` 顶注同族、`key-resolver.ts` 顶注「链只有一条」):两支方言各 * 抄一份准入判据,迟早在某一支上少一个分支——而少掉的那一支**没有任何报错面**,表现只是「这台机器上 * 一个陌生人可以把自己接到别人子代的转录上」。所以准入判据(两个方向)、列名、行→元组的映射都在 * 这一处;两支后端只负责在自己的 I/O 点上调用它。 * * ## 判据的两个方向(缺一不可,core `TtlSessionStore.assertPlacementAdmission` 的镜像) * * ① **claim 关门**:已 placed 的行拒绝 claim 形 acquire; * ② **镜像方向(pre-claim squat 竞态)**:子会话 id 在 spawn 帧上**先于**子代 prepare 公布,于是一次 * 并发的 claim 形 acquire 可能抢先建出**普通**会话,而后带 placement 的可信调用方拿到的就是一只 * 宿主可枚举、release 不真删的普通会话 ——「声明了 full tier 却在静默说谎」。⇒ 在**声明了分区**的 * 店上,带 placement 的 acquire 必须回读到**已持久化**且同一元组身份的行,回读缺失/异组=响亮拒。 */ import { type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord } from "@sema-agent/core"; /** * 两支 SQL 孪生的 placement **声明**(core `SessionStore.placements`)。 * * 🔴 声明制,不是鸭子类型:core 的铸造门判「row store ∧ session 店声明」,不声明的店**不收** placed * 语义(合法降级:placement 参数被忽略,会话就是普通会话)。所以这一格只有在上面六条义务**全部**在 * 本店实装之后才允许存在——它是一句关于本店行为的承诺,不是一个功能开关。 * `durable` 的依据:两支孪生的会话史落在 `session_meta`+`session_event` 两张真表上,进程重启后 * `acquire(id,{requireExisting:true})` 从事件日志重放(义务 1 的机器证在 * `test/session-placement-db-integration.test.ts` 的跨进程复活格)。 */ export declare const SQL_SESSION_PLACEMENTS: { readonly subagent: { readonly durability: "durable"; }; }; /** * `session_meta` 上承载 placement 元组的六列,**两方言同名**(单点定义:列名手抄两份必漂,而漂掉的 * 那一支只会表现为「这台机器上 placed 行读不出来」——静默,不报错)。 * `placed_at` 是 GC 的年龄锚(core `PlacedSessionRow.placedAt`);其余五列是元组本身。 */ export declare const PLACEMENT_SELECT_COLUMNS = "placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at"; /** 一行 `session_meta` 的 placement 列(两方言的驱动都把列名原样带回,故一份形)。 */ export interface PlacementColumns { placement_kind?: unknown; placement_scope?: unknown; placement_parent_session_id?: unknown; placement_root_session_id?: unknown; placement_handle?: unknown; placed_at?: unknown; } /** * 行 → 持久化的 placement 元组(`undefined` = 这是一只普通会话)。 * * 🔴 判别位是 `placement_kind`:它在场即 placed。`placed_at` 缺失(带外 SQL 写进半只元组)时回落 * `0` 而**不是**把整只元组读成缺席——把「元组半缺」读成「没有 placement」等于让义务 2/4/6 在那一行上 * 集体失效(宿主枚举会重新列出它、release 退回租约语义);`listPlaced` 那一侧另有 `tupleIncomplete` * 的诚实变体负责让 GC 对它 fail-closed。 */ export declare function placementFromRow(row: PlacementColumns | undefined): SessionPlacementRecord | undefined; /** * 行 → 一条 {@link PlacedSessionRow}(`listPlaced` 的行形)。 * * core 契约:join 键是**完整**的 `(scope, handle)`;缺任何一半 ⇒ 返回 `tupleIncomplete` 的诚实变体, * 消费方对它 fail-closed(永不按年龄收割)。一个「无行」与「另一个租户有行」分不清的 join 不许删东西。 */ export declare function placedRowFromRow(sessionId: string, row: PlacementColumns, lastActivityMs?: number): PlacedSessionRow; /** 创建时一次落行的六个绑定值(顺序 = {@link PLACEMENT_SELECT_COLUMNS});无 placement ⇒ 六个 NULL。 */ export declare function placementInsertValues(placement: SessionPlacement | undefined, placedAt: Date): [string | null, string | null, string | null, string | null, string | null, Date | null]; /** `acquire` 的 opts 面(core `SessionStore.acquire` 的第二参,窄成本模块要判的两键)。 */ export interface PlacementAcquireOpts { requireExisting?: boolean; placement?: SessionPlacement; } /** * 准入判据 —— **两个方向**(顶注 ①②;core `TtlSessionStore.assertPlacementAdmission` 的 SQL 镜像, * 逐条同义,文案自铸)。 * * @param persisted 该 id 在库里**已持久化**的元组(缺席 = 普通会话 / 无此行)。 * * · 方向①(claim 关门):`persisted` 在场 + 非 `requireExisting` + 调用方没带创建 placement ⇒ 拒。 * `requireExisting`(读/复活形,义务 5)与「创建调用及其幂等重取」(带 placement 的可信内部链) * 是仅有的两个合法形。placement 不可变意味着带 placement 的参数**永远重写不了**任何东西——持久化 * 的那一份恒赢,所以放行它不开任何口子。 * · 方向②(镜像/squat):带 placement 的创建形回读到**缺席**或**异组**元组 ⇒ 拒。缺席 = 并发的 * claim 形抢先建出了普通会话(或带外写把元组抹了);异组 = 同 id 上坐着另一条委派链的转录。 * 两者都不许让「声明了 durable 分区」这句话在静默中变成谎。 */ export declare function assertPlacementAdmission(sessionId: string, persisted: SessionPlacementRecord | undefined, opts?: PlacementAcquireOpts): void; /** * 一次 `listPlaced` 最多带回的行数。 * * core 契约不设上界,但两支孪生面对的是一张可以很大的真表:无界 SELECT 会让一次 reap tick 把整个 * placed 分区拉进内存。行按**年龄升序**(最老先出)⇒ 截断的语义是「本 tick 先收最老的一批,下一 tick * 接着收」,而不是「有些行永远看不见」。判据消费方(core 的分区收割腿)本就是每 tick 重新枚举的。 */ export declare const LIST_PLACED_BATCH = 1000; //# sourceMappingURL=session-placement.d.ts.map