import type { WorkflowRun } from "@sema-agent/core"; /** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */ export interface WorkflowAgentSessionRow { /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = 那条 checkpoint 行的 `sessionId`; * **不是** `sourceTaskId` —— 后者在 core 契约上是 ECHO-ONLY、明令不可作门输入)。 */ sessionId: string; /** 它所属的 workflow run(`w*`)。 */ runId: string; /** 该 agent 调用的稳定顺位身份(`WorkflowAgentRun.callKey`)—— 行自描述用,取 run 后仍以行内实体为准。 */ agentKey: string; /** run 的租户键(`WorkflowRun.scope`)。 */ scope: string; } /** * 索引面的两个动词。**写者只有一个**(见文件头),读者是判别器/收敛器/读面。 * * `syncRun` 语义 = 「这条 run 当前的 parked 行集**就是** rows」:多出来的旧行删,rows 里的行 upsert。 * 幂等、可重放、与 run 的一次成功写同拍。 */ export interface WorkflowAgentSessionIndex { syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise; findByAgentSession(sessionId: string): Promise; } /** * 🔴 **park 恢复身份的单一真源**(core 7.17.0 [ref])—— 引擎写在记录上的 `parks[]` 的元素形。 * * 型从 core 的那一位取、不在本仓复述:core 往元素上加一位 ⇒ `trace/core-keyset-guard.ts` 的 ⑬b 成员门 * 与下面那只读者的构造式**同时**红,加键的人二选一,没有第三条静默的路。 */ export type WorkflowRunParkKey = NonNullable[number]; /** * 盘上 run blob → `parks[]` 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形;与本文件 * {@link readAgentSessionLedgerLine} 同一条姿态)。 * * 判据(7.78.0 / core 7.18.0 [ref] 复核后的口径): * · 这一位**不是数组**(旧引擎写的记录 / 被投影剥掉)⇒ **本读者**读不出来 ⇒ 答缺席,由调用方响亮拒。 * ⚠️ 这已**不再**等于「core 也拒 resume」:core 7.18.0 起对 `parks` 缺席的记录从 journal 的 `parked` * 臂 + 记录自己 `parked` 腿行的 `parkedCheckpointToken` 派生候选并逐个验真,非空即准入(三臂的第二臂)。 * 本读者仍答缺席的理由是另一条:那条派生只发生在**准入期**,赎回键从记录上读不出来 —— 服务端要么拒, * 要么在自己这边重算 core 的三臂(被 [ref] 统一掉的双读)。恢复动作因此是「去 resume 这条 run」, * 逐字见 `parked-decide.ts` 的 `denyWorkflowParksUnreadable` 顶注; * · **任一条目形不合 ⇒ 整只读不出来**(整档成败制,与本文件账本行同判)。core 对同一形抛 * `park_binding_broken` 整条拒 resume;「丢掉坏的、留下好的」会静默铸出一份**比真相少几条 park** 的行集, * 而这只读者的消费面正是 park 的赎回路由。 * * ⚠️ 「读不出来」与「读得出来、只是空的」是**两个状态**:后者 = 这条 run 不负责任何 park(诚实 miss), * 前者 = 证不了,两者的处置相反 ⇒ 缺席永远不折成 `[]`。 */ export declare function readRunParks(run: unknown): WorkflowRunParkKey[] | undefined; /** 派生的**唯一属主**:一只 `WorkflowRun` 的 park **路由**行。 * * 🔴 **S-252 的分工线**(两轮 codex [high] 各证伪了一次「把这两件事并成一件」的尝试,红格在 * `test/workflow-park-decide-lane.test.ts`): * · **路由**(本函数)回答「**哪一跑**把这个子会话 park 了」—— 一件关于**这一跑执行过什么**的事实。 * 它只有一个可靠的载体:`agents[]` 上那一行的执行状态(core 的 `parkAgentLeg` 在写 `parks` 条目的 * 同一拍把 `rec.status = "parked"` / `rec.sessionId` 落上去)。 * · **赎回权威**(「这个 token 是不是这条 run 负责的一条 park」)只由记录上的 `parks[]` 回答 * ——{@link readRunParks} 是它唯一的读者,消费点是 `parked-decide.ts` 的判别式。 * ⇒ 恢复身份(赎回键)自此**只有一个家**;本函数一个 token 字节都不读、也不存(索引只回答「去哪条 run 找」)。 * * 为什么路由不能改读 `parks[]`(两条都是真跑复现出来的,别再合并): * · `parks` 含**继承而未由本跑执行**的条目:core 在读 checkpoint 真值**之前**就把它们写进新一跑的记录 * (`dist/orchestration/workflow.js`,`parks = inherited` 发生在读真值的循环**之前**),那一读抛 ⇒ 同处的 catch 落一条 * **带 parks 而 journal 为空**的 `failed` 记录。跟着 `parks` 走 ⇒ 坐标被它抢走 ⇒ `/decide` 让宿主 resume 它 * ⇒ 已完成的前缀**整段重跑**(非幂等副作用重复执行)。 * · 而「只收 `originRunId === runId`」同样不成立:re-park 时 core 按 callKey 就地替换条目、`originRunId` * **沿用继承来的那一个**,只在 `journalAppendParked` 成功之后才改写成本跑 id,而 * `persist("update")` 打在 append **之前**。⇒ 那条判据会在 append 失败(以及 append 前的那一拍)上把 * 真正持有**新 token** 的那一跑整只排除,而上一跑的记录里只有**旧** token ⇒ 这条审批再也送不出去。 * * 准入判据(两位,与 `parkAgentLeg` 的落位一一对应):行 `status === "parked"` ∧ 有非空 `sessionId` * (checkpoint 那一侧的 join 键就是它)。`callKey` 缺席写空串 —— 它是行自描述,不是 join 键。 */ export declare function buildParkedAgentSessionRows(runId: string, run: { scope: string; agents?: ReadonlyArray<{ status?: unknown; sessionId?: unknown; callKey?: unknown; }>; }): WorkflowAgentSessionRow[]; /** 两处(装饰器的变更检测、local 后端的重放)共用的行集等值判据 —— 顺序无关的集合比较,单一属主。 */ export declare function sameAgentSessionRows(a: readonly WorkflowAgentSessionRow[], b: readonly WorkflowAgentSessionRow[]): boolean; /** 账本一行的**内域形**:一次 `syncRun` 的整份行集快照(`{runId, rows}`)。 */ export interface WorkflowAgentSessionLedgerLine { runId: string; rows: WorkflowAgentSessionRow[]; } /** * 盘上账本行 → 内域形的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形)。 * * 姿态与 SQL 孪生逐字同源:那边 `findByAgentSession` 逐字段收窄(`String(r.session_id)` …),这边逐字段 * 判形 —— 一张索引面、两个后端、**一种**入域姿态。 * * **整行成败制**(与既有的撕裂行臂同一条规则):一行 = 一次 syncRun 的**整份**快照,任一位读不出来就说明 * 这份快照重建不出来 ⇒ 整行不 apply(那条 run 保留上一份已知良好的快照)。不做「丢掉坏成员、留下好成员」 * ——那会静默铸出一份**比真相少几条 park** 的行集,而这条索引的消费面正是 park 的赎回路由。 * 读不出来 ⇒ 调用方**响亮**记一笔([ref]:安全轴不许静默),不吞整档。 */ export declare function readAgentSessionLedgerLine(v: unknown): WorkflowAgentSessionLedgerLine | undefined; /** * 单机车道的索引(`FileWorkflowRunStore` / `InMemoryWorkflowRunStore` 的同室孪生)。 * * `root` 在场 ⇒ 追加式 JSONL 账本(与姊妹 {@link import("./workflow-notify-journal.js").FileWorkflowNotifyJournalStore} * 逐字同形:0o700 目录 + 0o600 账本、按行重放、末行撕裂即丢),重启后 park 仍可 join;缺席 ⇒ 纯内存 * (`WORKFLOW_RUN_STORE=memory` 的显式易失车道,与它的 run 店同寿命)。 * * 账本的一行 = **一次 `syncRun` 的整份行集**(按 runId 后写覆盖),所以重放即收敛,不需要压缩腿。 */ export declare class LocalWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex { private readonly fsyncEnabled; /** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的 * `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */ private readonly onWarn?; private readonly ledgerPath; private readonly byRun; private readonly bySession; private fd; constructor(root?: string, fsyncEnabled?: boolean, /** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的 * `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */ onWarn?: ((msg: string, meta: Record) => void) | undefined); /** * 打开账本时把**撕裂尾**(崩在 append 中途、末尾没有换行的那截字节)截回最后一个完整行边界。 * * 🔴 为什么必须在**任何 append 之前**做(codex 修复验证轮 [medium],故障注入复现):残尾留在盘上时, * 下一次 `syncRun` 会紧贴着它落字节 ⇒ `<残尾><新记录>` 在 replay 里是**一行**,JSON 解析失败被整行丢掉。 * 丢的**不是**那截未提交的残尾(丢它是对的),而是紧随其后那次**成功重试**写下的行 —— 于是「写失败了 * 下一次写会重试救回来」这条本模块与 `syncAgentSessionIndex` 都在依赖的性质当场失效,park 索引静默消失。 * 截尾之后 append 永远从一个干净的行边界开始,重试才真的救得回来。 * * 截掉的只可能是**未提交**的字节(一条记录的耐久性以它那个换行为界,与 notify journal 同一条崩溃序), * 所以这里不是丢数据;但它是一次真实的崩溃留痕,**响亮**一次([ref]:安全轴不许静默)。 */ private truncateTornTail; private replay; /** 内存两张表的收敛(重放与写路径共用一份逻辑)。 */ private apply; syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise; findByAgentSession(sessionId: string): Promise; } //# sourceMappingURL=workflow-agent-session-index.d.ts.map