vicTop-cw/fist-mbt/src/store does not have a README file

    Store

    pub trait Store {
    fn create_task(Self,
    Task
    ) -> Result[Unit, String]
    fn get_task(Self, String) ->
    Task
    ?
    fn update_task(Self,
    Task
    ) -> Result[Unit, String]
    fn list_tasks(Self) -> Array[
    Task
    ]
    fn delete_task(Self, String) -> Result[Unit, String]
    fn clear(Self) -> Unit
    }

    仓库统一接口

    AgentRecord

    pub struct AgentRecord {
    agent_id : String
    agent_type : String
    role : String
    soldier : String
    pid : Int
    pubkey_fp : String
    first_seen : String
    last_seen : String
    status : String
    }

    AgentRecord —— agents 表的一行(进程级身份;agent_id 即指纹哈希,跨进程持久)。 pid 是进程实例标记(本仓不暴露 OS-PID,由 server 层以启动毫秒推导的实例 token 注入), 同一指纹换 pid 视为"同一身份换进程"——身份不变、活体续期;pid 与上次不同即判旧实例已死。

    AssistantReg

    pub struct AssistantReg {
    agent : String
    mode : String
    is_primary : Bool
    registered_at : String
    heartbeat_at : String
    status : String
    } derive(Eq,
    Debug
    )

    项目助理登记(multi-agent-lock v3 阶段2 精简版)。

    AssistantReg::equal

    AssistantReg::new

    fn AssistantReg::new(agent~ : String, mode~ : String, is_primary~ : Bool, registered_at~ : String, heartbeat_at~ : String, status~ : String) -> AssistantReg

    便捷构造(AssistantReg 字段对外只读,跨包经此构造/更新)。

    AssistantReg::not_equal

    fn AssistantReg::not_equal(x : AssistantReg, y : AssistantReg) -> Bool

    AssistantReg::to_json

    fn AssistantReg::to_json(self : AssistantReg) -> Json

    AssistantReg → JSON(供 MCP 工具输出)。

    ExecutionRecord

    pub struct ExecutionRecord {
    id : String
    task_id : String
    executor : String
    model : String
    tokens_in : Int
    tokens_out : Int
    cost : Double
    duration_ms : Int
    rate_limited : Bool
    failure_reason : String
    created_at : String
    }

    执行记录结构体

    FileLock

    pub struct FileLock {
    lock_id : String
    batch_id : String
    scope : String
    agent : String
    status : String
    ts : String
    ttl_until : String
    meta : String
    } derive(Eq,
    Debug
    )

    文件锁记录(跨后端统一结构)。

    FileLock::agent_of

    fn FileLock::agent_of(self : FileLock) -> String

    BUG-424:FileLock.agent 的读访问器。

    为什么需要它:本 struct 的字段都是包内私有(无 pub 修饰)⇒ engine 侧跨包 既不能写 struct literal、也不能直接读字段(BUG-413 已实测编译器拒绝 4036/4033)。 而 engine_rsv.mbt 的锁权限门必须读「谁要这把锁」来判角色。

    只读不改:不提供 setter,避免绕开 renew 的时序语义。

    FileLock::equal

    fn FileLock::equal(FileLock, FileLock) -> Bool

    FileLock::new

    fn FileLock::new(lock_id~ : String, batch_id~ : String, scope~ : String, agent~ : String, status? : String, ts? : String, ttl_until? : String, meta? : String) -> FileLock

    便捷构造。

    FileLock::not_equal

    fn FileLock::not_equal(x : FileLock, y : FileLock) -> Bool

    FileLock::renew

    fn FileLock::renew(self : FileLock, ttl_until~ : String, ts~ : String) -> FileLock

    续期:返回同锁的新 ttl_until + ts,status 重置为 active。

    FileLock::scope_of

    fn FileLock::scope_of(self : FileLock) -> String

    BUG-424:FileLock.scope 的读访问器(理由同 agent_of)—— 权限门要拿 scope 去查规则表。

    FileLock::status_of

    fn FileLock::status_of(self : FileLock) -> String

    BUG-424:FileLock.status 的读访问器。

    用途:locks_try_acquire 的 legacy_grant 特判要判「这条现有锁是不是还活着」 (status == "active")—— 只有活着且属于自己的锁才允许在权限门之后续期。

    只读不改:本仓惯例(同 agent_of / scope_of)—— 不给 setter, 改 status 必须走 renew / locks_try_acquire 那条带时序语义的路。

    FileLock::to_json

    fn FileLock::to_json(self : FileLock) -> Json

    FileLock → JSON(供 MCP 工具输出)。

    FileLock::to_repr

    LockAcquireOutcome

    pub struct LockAcquireOutcome {
    granted : Bool
    action : String
    prev_agent : String
    agent : String
    ttl_until : String
    violation_logged : Bool
    } derive(
    Debug
    )

    锁获取裁决结果(与 RsvOutcome 同一纪律:结论来自写后回读,不由请求反推)。

    LockAcquireOutcome::denied_by_permission

    fn LockAcquireOutcome::denied_by_permission() -> LockAcquireOutcome

    BUG-424:权限拒绝态的规范构造(granted=false + action="permission_denied")。

    与 relabel 的分工:relabel 用于「拿到锁但改标签」(如 unregistered advisory), 本函数用于「根本没有碰库就被权限门挡住」——此时库里没有对应行,agent/ttl_until 必须为空(不许由请求反推,否则回执会谎报「库里现值」)。

    为什么不复用 relabel:拒绝态没有「原 outcome」可改(门的判断在写库之前)。

    LockAcquireOutcome::relabel

    fn LockAcquireOutcome::relabel(self : LockAcquireOutcome, action : String, granted : Bool) -> LockAcquireOutcome

    BUG-424:锁裁决结果的受控改写(与 RsvOutcome::relabel 同款手法)。

    为什么需要它(这是那条「暂时没接」注释的真实原因,它一直说错了): engine_rsv.mbt 里挂着一句「锁的权限门禁暂时没接——LockAcquireOutcome 是 read-only 类型,必须在 store 内部创建;将来会在 store_rsv 层加一个 permission_denied 构造器」。 该注释写于 2026-10-22,挂了 4 天,而它的理由从来就不成立: ① 本结构体没有任何 read-only 标记(grep read-only\|readonly → 0 命中),它是普通 pub struct; ② 真正的限制只有一条:字段是包内私有的 ⇒ engine_rsv 跨包写 struct literal 会被编译器 拒绝(BUG-413 实测 4036/4033); ③ 而同一个障碍 BUG-413 已经用 RsvOutcome::relabel 解决过一遍 —— 只是没人回头看那条注释。 ⇒ 「等后端扩展」是伪需求:在本文件补一个同款 helper 即可。

    本函数不改变任何裁决事实 —— agent / ttl_until / prev_agent / violation_logged 一律原样透传(谁持有、持到什么时候,仍由库里现值说话);只改 action 与 granted, 因为锁的权限门需要表达「拒绝」(granted=false + action="permission_denied"), 而预订那边 RoleDenied 是走 Err —— 两条通道形状不同,故本函数比 RsvOutcome::relabel 多一个 granted 参数。

    LockViolation

    pub struct LockViolation {
    ts : String
    agent : String
    scope : String
    reason : String
    lock_id : String
    } derive(Eq,
    Debug
    )

    锁违例记录。

    LockViolation::equal

    LockViolation::new

    fn LockViolation::new(ts~ : String, agent~ : String, scope~ : String, reason~ : String, lock_id~ : String) -> LockViolation

    便捷构造(LockViolation 字段对外只读,跨包经此构造)。

    LockViolation::not_equal

    fn LockViolation::not_equal(x : LockViolation, y : LockViolation) -> Bool

    LockViolation::to_json

    fn LockViolation::to_json(self : LockViolation) -> Json

    LockViolation → JSON。

    MemoryStore

    pub struct MemoryStore {
    tasks : Map[String,
    Task
    ]
    specs : Map[String, SpecRecord]
    default_ns : String
    call_logs : Array[Json]
    reservations : Map[String, (String, String, String)]
    executors : Map[String, (String, String)]
    file_locks : Map[String, FileLock]
    lock_violations : Array[LockViolation]
    assistant_reg : Map[String, AssistantReg]
    circuit_breakers : Map[String, (String, Int, Int, Int, Int, Int, Int)]
    saga_log : Map[String, Array[(Int, String, String, String, String)]]
    saga_next_id : Int
    challenges : Map[String, (String, Bool)]
    }

    内存实现:进程内 Map,简单可靠,配合单机 MCP server 足够。

    MemoryStore::assistant_get

    fn MemoryStore::assistant_get(self : MemoryStore, agent : String) -> AssistantReg?

    内存后端:查询某 agent 的助理登记(不存在返回 None)。

    MemoryStore::assistant_list

    fn MemoryStore::assistant_list(self : MemoryStore) -> Array[AssistantReg]

    内存后端:列出全部助理登记。

    MemoryStore::assistant_set_status

    fn MemoryStore::assistant_set_status(self : MemoryStore, agent : String, status : String) -> Bool

    内存后端:改某 agent 登记的 status(dead / resigned 等;不存在返回 false)。

    MemoryStore::assistant_upsert

    fn MemoryStore::assistant_upsert(self : MemoryStore, reg : AssistantReg) -> Unit

    内存后端:写入/覆盖一个助理登记(幂等)。

    MemoryStore::clear

    fn MemoryStore::clear(self : MemoryStore) -> Unit

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::create_task

    fn MemoryStore::create_task(self : MemoryStore, task :
    Task
    ) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::delete_task

    fn MemoryStore::delete_task(self : MemoryStore, id : String) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::executor_clear

    fn MemoryStore::executor_clear(self : MemoryStore) -> Bool

    内存后端:清空全部执行者能力注册(Marketplace 重置/整洁)。

    MemoryStore::executor_list

    fn MemoryStore::executor_list(self : MemoryStore) -> Array[(String, String, String)]

    内存后端:列出全部执行者 (name, abilities_json, created_at)。

    MemoryStore::executor_save

    fn MemoryStore::executor_save(self : MemoryStore, name~ : String, abilities_json~ : String, created_at~ : String) -> Result[String, String]

    内存后端:写入/覆盖一个执行者的能力注册(幂等)。

    MemoryStore::get_spec

    fn MemoryStore::get_spec(self : MemoryStore, id : String) -> SpecRecord?

    按 id 读取单条语料记录。

    MemoryStore::get_task

    fn MemoryStore::get_task(self : MemoryStore, id : String) ->
    Task
    ?

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::list_specs_by_task

    fn MemoryStore::list_specs_by_task(self : MemoryStore, task_id : String) -> Array[SpecRecord]

    列出某任务的全部语料 / 复验记录。

    MemoryStore::list_tasks

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::list_tasks_in

    fn MemoryStore::list_tasks_in(self : MemoryStore, ns_filter : String) -> Array[
    Task
    ]

    列出指定命名空间的任务

    MemoryStore::lock_violations_list

    fn MemoryStore::lock_violations_list(self : MemoryStore) -> Array[LockViolation]

    内存后端:列出违例记录。

    MemoryStore::lock_violations_push

    fn MemoryStore::lock_violations_push(self : MemoryStore, v : LockViolation) -> Unit

    内存后端:追加一条锁违例(助理接管等治理动作落案底)。

    MemoryStore::locks_get

    fn MemoryStore::locks_get(self : MemoryStore, scope : String) -> FileLock?

    内存后端:查询某 scope 的锁(不存在返回 None)。

    MemoryStore::locks_list

    fn MemoryStore::locks_list(self : MemoryStore) -> Array[FileLock]

    内存后端:列出全部锁。

    MemoryStore::locks_release

    fn MemoryStore::locks_release(self : MemoryStore, scope : String, agent : String) -> Bool

    内存后端:释放某 scope 的锁(仅持有者可释放;不存在或非持有者返回 false)。

    MemoryStore::locks_sweep

    fn MemoryStore::locks_sweep(self : MemoryStore, now : String) -> Int

    内存后端:清理过期锁(status=active 且 ttl_until < now → 标记 expired,记违例)。 返回被清理的锁数量。

    MemoryStore::locks_try_acquire

    fn MemoryStore::locks_try_acquire(self : MemoryStore, lock : FileLock) -> LockAcquireOutcome

    内存后端:按裁决抢锁(BUG-145)——抢不到就是拒绝,不是覆盖。 形状与 rsv_try_set 逐字同构:写前快照过 lock_acquire_action(委托 rsv_action) 定标签,结论一律由写后回读定——回执不许由请求反推。 conflict 时落一条 lock_denied 违例(拒夺审计),锁行不动,现持有者原样保住。

    MemoryStore::locks_upsert

    fn MemoryStore::locks_upsert(self : MemoryStore, lock : FileLock) -> Unit

    内存后端:把锁写入(按 scope 唯一,覆盖旧值;旧锁若 active 且非本人且未过期则记违例)。

    这是底层原语,不是互斥门:它永远落库(last-write-wins),只在覆盖他人 live 锁时 留一条 lock_taken_over 事后审计。要「抢不到就拒」请用 locks_try_acquire。

    MemoryStore::new

    MemoryStore::recent_call_logs

    fn MemoryStore::recent_call_logs(self : MemoryStore, limit? : Int) -> Array[Json]

    读取最近 N 条调用日志(新的在前)。

    MemoryStore::rsv_get

    fn MemoryStore::rsv_get(self : MemoryStore, scope : String) -> (String, String, String)?

    内存后端:查询某作用域预订(未预订返回 None)。

    MemoryStore::rsv_list

    fn MemoryStore::rsv_list(self : MemoryStore) -> Array[(String, String, String)]

    内存后端:列出全部预订(升序,无指定顺序保证)。

    MemoryStore::rsv_release

    fn MemoryStore::rsv_release(self : MemoryStore, scope : String, agent : String) -> Bool

    内存后端:释放预订(仅持有者可释放;不存在或非持有者返回 false)。

    MemoryStore::rsv_set

    fn MemoryStore::rsv_set(self : MemoryStore, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[String, String]

    内存后端:写入/覆盖某作用域预订。(返回值约定:Ok(msg),不使用)

    MemoryStore::rsv_try_set

    fn MemoryStore::rsv_try_set(self : MemoryStore, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[RsvOutcome, String]

    内存后端:按裁决写预订,写后回读收口。 旧形状(先 rsv_get 判空再无条件 rsv_set)在跨进程下会双方都读到空闲、双方都写成功; 内存后端本来没有那个窗口,但两后端必须同一形状,否则单测永远绿而 SQLite 红(BUG-85/BUG-79 的旧例)。

    MemoryStore::update_spec_status

    fn MemoryStore::update_spec_status(self : MemoryStore, id : String, status : String, content : String, updated_at : String) -> Result[Unit, String]

    更新语料状态(可同时更新内容与时间戳)。

    MemoryStore::update_task

    fn MemoryStore::update_task(self : MemoryStore, task :
    Task
    ) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法(避免 deprecated 隐式提升)。

    MemoryStore::update_task_if_status

    fn MemoryStore::update_task_if_status(self : MemoryStore, task :
    Task
    , expected_status : String) -> Result[Unit, String]

    MemoryStore::upsert_spec

    fn MemoryStore::upsert_spec(self : MemoryStore, rec : SpecRecord) -> Result[Unit, String]

    写入 / 覆盖一条语料记录(主键 id)。

    MemoryStore::write_call_log

    fn MemoryStore::write_call_log(self : MemoryStore, ts~ : String, tool~ : String, caller~ : String, ns~ : String, params_json~ : String, result_json~ : String, ok~ : Bool, runtime_ms~ : Int, seq~ : Int) -> Result[Unit, String]

    写入一条调用日志(MemoryStore 进程内 array,软上限 ~500,超限丢最早一条)。

    MultiStore

    pub struct MultiStore {
    stores : Map[String, SqliteStore]
    data_dir : String
    }

    FIST-Mbt store: 命名空间多库持久化管理器。 按 ns 路由到独立 SQLite 文件 {data_dir}/{ns}.db,惰性打开(首次访问才建库)。 每个 ns 拥有独立的物理库文件,实现多租户/多项目隔离;路径完全由参数传入, 不做任何绝对路径硬编码。

    可变性模式与 AuditLog 一致:通过 self.stores.set / self.stores.remove 原地修改, 无需在调用方重新赋值。这样可被模块级不可变绑定持有,供 MCP 工具闭包安全调用。

    MultiStore::close

    fn MultiStore::close(self : MultiStore, ns : String) -> Unit

    关闭并移除某个命名空间(不删除物理文件,可重新 open 恢复)。

    真关句柄(BUG-463):过去只 stores.remove(ns),底层 @sqlite.Database 句柄仍被引用着(Map 摘掉 ≠ 释放),Windows 上该 .db 的写锁/文件锁一直不放。 SqliteStore::close() 幂等,故本函数重复调用安全。

    MultiStore::close_all

    fn MultiStore::close_all(self : MultiStore) -> Unit

    关闭全部命名空间(逐库真关句柄,幂等)。

    BUG-463:旧实现只 stores.remove(k),句柄一个都没释放;close_all 之后 进程内仍握着全部 ns 的 sqlite 连接 ⇒ 测试夹具删 .db 失败、重开同库可能撞锁。 现在逐个走 MultiStore::close(它对每个 ns 都调 SqliteStore::close())。

    MultiStore::data_dir_ok

    fn MultiStore::data_dir_ok(dir : String) -> Bool

    库文件根目录校验(BUG-78):只接受相对 server 进程 cwd 的相对路径。 拒空串 / .. 上跳 / 前导分隔符 / 盘符(C:)——过去这里只对 ns 走 safe_ns, data_dir 原样拼进 \{dir}/\{safe}.db 就交给 SqliteStore::open(open 会建文件), 于是"命名空间物理隔离"实际是"调用方指到哪就往哪建库"。 口径与 src/server/bugreport.mbt 的 bug_project_dir_ok 保持一致: 同族守卫不该有两套规则(project_dir 拒穿越、data_dir 放行,就是本次缺陷的形状)。

    MultiStore::get

    fn MultiStore::get(self : MultiStore, ns : String) -> SqliteStore?

    获取某 ns 的存储;惰性打开:尚未打开时按 self.data_dir 首次创建/打开, 并在打开成功后缓存进 self.stores,保证后续 get/ns_stats 复用同一句柄。 ns 非法(未过 safe_ns)或打开失败返回 None。

    MultiStore::new

    fn MultiStore::new(data_dir? : String) -> MultiStore

    构造:仅记录根目录,不打开任何库(惰性)。 默认 ".fist-mbt/data"(相对 server 进程 cwd,经 data_dir_ok 校验):server 由 .mcp.json 以目标项目根为 cwd 启动,故默认落点即 <target>/.fist-mbt/data (.fist-mbt/ 机器独有、gitignore;见 scripts/fist-init.py 双骨架)。调用方显式传 data_dir 仍完全按传入值落盘(测试/scratch 隔离零回归)。

    MultiStore::ns_list

    fn MultiStore::ns_list(self : MultiStore) -> Array[String]

    列出已打开的命名空间(Map 遍历顺序,不保证稳定)。

    MultiStore::ns_stats

    fn MultiStore::ns_stats(self : MultiStore) -> Json

    每个已打开 ns 的任务数统计(直接用该 ns 库的 tasks 表 COUNT)。 返回 Json 数组,元素形如 {"ns": "...", "total": N}。

    MultiStore::ns_task_count

    fn MultiStore::ns_task_count(self : MultiStore, ns : String) -> Int

    某个命名空间库里的任务总数(未打开 / 打开失败 / 无句柄一律 0)。

    BUG-420:store_open 回执要给出「本库几条 vs 主库同标签几条」这对读数 —— 过去这两个数只能靠外部 sqlite3 直查(诊断报告就是这么拿到 148 vs 0 的)。 注意这里是惰性打开(复用 MultiStore::get 语义):调用方应先把 ns open 好。

    MultiStore::open

    fn MultiStore::open(self : MultiStore, ns~ : String, data_dir~ : String) -> Bool

    打开(或复用)某个命名空间对应的库文件 {data_dir}/{safe_ns}.db。 ns 会先经 safe_ns 消毒,非法命名空间直接失败返回 false。 data_dir~ 为空时回退到 self.data_dir(与 get 保持一致,避免状态分裂); 两者取用后的最终目录仍要过 data_dir_ok(BUG-78:消毒范围覆盖实际落点,不只覆盖入参形态)。

    MultiStore::safe_ns

    fn MultiStore::safe_ns(ns : String) -> String?

    命名空间消毒:仅允许 [A-Za-z0-9_-],拒绝空串及任何含(斜杠/反斜杠/点/ 控制字符/空白)的输入,杜绝路径遍历(../../etc/x)与注入。 合法返回浅拷贝后的 ns;非法返回 None。

    RsvOutcome

    pub struct RsvOutcome {
    reserved : Bool
    action : String
    prev_agent : String
    agent : String
    ttl_until : String
    }

    预订裁决结果。agent/ttl_until 一律是库里现值(conflict 时即 held_by), 不是调用方想要的那个值——回执不许由请求反推。

    RsvOutcome::relabel

    fn RsvOutcome::relabel(self : RsvOutcome, action : String) -> RsvOutcome

    只改 action 标签、其余字段原样保留(BUG-413)。

    为什么需要这个而不是让调用方自己构造:RsvOutcome 的字段是包内私有的, 跨包 struct literal 会被编译器拒绝(实测 4036/4033)。 而 engine_rsv 需要在「未登记路径 advisory 登记成功」时把 action 标成 unregistered, 好让调用方分辨「这是登记通过」而不是「授权通过」。

    本函数不改变 reserved —— 只改标签,不改裁决。

    SpecRecord

    pub struct SpecRecord {
    id : String
    task_id : String
    spec_type : String
    content : String
    status : String
    created_at : String
    updated_at : String
    } derive(Eq,
    Debug
    )

    语料 / 复验记录(跨后端统一结构)。

    SpecRecord::equal

    fn SpecRecord::equal(SpecRecord, SpecRecord) -> Bool

    SpecRecord::new

    fn SpecRecord::new(id~ : String, task_id~ : String, spec_type? : String, content? : String, status? : String, created_at? : String, updated_at? : String) -> SpecRecord

    便捷构造(仅 id / task_id 必填,其余带默认值)。

    SpecRecord::not_equal

    fn SpecRecord::not_equal(x : SpecRecord, y : SpecRecord) -> Bool

    SpecRecord::to_json

    fn SpecRecord::to_json(self : SpecRecord) -> Json

    记录转 JSON(供 MCP 工具输出,content 不展开以免撑爆响应)。

    SqliteStore

    pub struct SqliteStore {
    db :
    Database
    ?
    closed : Bool
    }

    SQLite 后端:持有打开的 Database(Option 以承载 open 失败场景)。

    字段为什么是 mut(BUG-463):close() 必须把 Some(db) 归零成 None, 否则「已关闭」这件事在类型里无处可存 —— 既做不出幂等(二次 close 会对着已释放的 底层句柄再 close 一次),也做不出「关闭后既有操作返回 Err 而非 panic」。 归零后全文件 60 处 match self.db 的 None 分支自动接手:返回 Err("sqlite store 未打开") / 空集合 / false(见各函数),全文件零 abort。

    closed 与 db = None 不是同一件事(BUG-463 回归面):db == None 只说明 「当前没有可用句柄」,而底层句柄可能已被外部先关掉(白盒测试直接 match s.db { Some(db) => db.close() } 是合法写法,store 无从得知)。那种情况下 db 仍是 Some(已关闭句柄),再 close 一次会让 node:sqlite 抛 database is not open ⇒ 进程级未捕获异常。closed + 容错关闭一起保证对外语义: close() 恒成功、可重复调用,两种幂等面(自己关过 / 别人先关过)都覆盖。

    SqliteStore::agents_acl_check

    fn SqliteStore::agents_acl_check(self : SqliteStore, agent_id~ : String, ns~ : String, write~ : Bool, current_pid~ : Int) -> Result[Unit, String]

    ACL 检查(调用前先 agents_acl_ensure)。write=true 且 can_write=0 即 Err; write=false 且 can_read=0 即 Err。pid_bound=1 且 agents.pid 与当前 pid 不同视为换实例 → Err。

    SqliteStore::agents_acl_count_writable

    fn SqliteStore::agents_acl_count_writable(self : SqliteStore, ns~ : String) -> Int

    统计某命名空间下 can_write=1 的 agent 数(BUG-411)。

    为什么需要它:publish / publish_parallel / dag_publish 走 human_steward 特权路径、不看 ns ACL,而 claim 走 agent_write_gate 的 ns ACL —— 两道门结论可以相反(同一 ns 内 publish 成功、claim 被拒)。调用方在 「发布成功」与「认领被拒」之间没有任何预警,排查成本高(BUG-411 实测)。

    本函数只回答一个问题:这个 ns 现在有没有任何一个 agent 能写。 publish 成功时若为 0,回执就该当场预警「你之后 claim 会被拒 + 出路」。

    口径与 agents_acl_check 保持一致:只看 can_write,不看 revoked_at (吊销在 agents_acl_grant 里就是 can_write=0,两处同源)。 内存 stub 返回 0(无持久化 ACL 面,与 agent_write_gate 的放行口径不同—— 那是写门,这是诊断读数,0 会让调用方看到「无持久化 ACL」这个事实)。

    SqliteStore::agents_acl_ensure

    fn SqliteStore::agents_acl_ensure(self : SqliteStore, agent_id~ : String, ns~ : String, now~ : String) -> Result[Unit, String]

    确保一条默认 ACL(can_read=1, can_write=0, pid_bound=0)——fail-closed 底线。 已存在则不动(授权变更只经 agents_acl_grant)。

    SqliteStore::agents_acl_grant

    fn SqliteStore::agents_acl_grant(self : SqliteStore, agent_id~ : String, ns~ : String, can_read~ : Bool, can_write~ : Bool, pid_bound~ : Bool, now~ : String, by~ : String) -> Result[Unit, String]

    授权/收权(UPSERT):can_read/can_write 直接覆盖;pid_bound=1 时绑定当前 agents.pid, 之后换进程实例即失效。now 写入 revoked_at/revoked_by 仅用于吊销记录(can_write=0 且 pid_bound=0)。

    SqliteStore::agents_get

    fn SqliteStore::agents_get(self : SqliteStore, agent_id : String) -> AgentRecord?

    读单个 agent 的持久化身份(无记录返回 None)。

    SqliteStore::agents_heartbeat

    fn SqliteStore::agents_heartbeat(self : SqliteStore, agent_id~ : String, agent_type~ : String, role~ : String, soldier~ : String, pid~ : Int, now~ : String) -> Result[Unit, String]

    进程级心跳:刷新 pid/last_seen 并置回 active。agent_id 未注册则先注册(agent_type 取调用方给出值)。

    SqliteStore::agents_register

    fn SqliteStore::agents_register(self : SqliteStore, agent_id~ : String, agent_type~ : String, role~ : String, soldier~ : String, pubkey_fp~ : String, pid~ : Int, now~ : String) -> Result[Unit, String]

    注册 agent(指纹即身份)。首次出现则 INSERT,已存在则仅刷新 pid/last_seen/status(身份不变,进程实例换新)。

    职责边界(BUG-404):本函数只写 agents 表,不落默认 ACL—— 原注释称「并落一条 fail-closed 的默认 ACL」,实际 body 无任何 agent_acl INSERT。 默认 ACL 的真源在 server 层的 agents_acl_ensure(src/server/server.mbt, agent_touch_first_seen 调用链保证),不在 store 层。 后果:任何绕过 server 层 agents_acl_ensure 的直连 store 调用 (现成的就是 agents_heartbeat)会留下「有 agent 身份、无 ACL 行」的中间态; agents_acl_check 对此返回 Err("ACL 无记录(fail-closed)")——仍 fail-closed, 不是安全洞,但「注释承诺的默认 ACL 自动落」在这条路径上从不发生。 需要「注册即落 ACL」语义时,调用方必须先过 server 层 agents_acl_ensure。

    SqliteStore::agents_role_for_judgment

    fn SqliteStore::agents_role_for_judgment(self : SqliteStore, agent_id : String) -> String?

    取「可用于角色裁决的 declared 角色」(BUG-422 B 案)。

    与 agents_get(agent_id).role 的差别只有一条,但就是这条决定它是不是越权口: · 未登记 ⇒ None(推断兜底) · 本进程自身份(agent_type == "server")⇒ None —— 报 fist-mbt 不再继承 Leader · 其余登记的 agent ⇒ Some(role),交给 resolve_agent_role 做「可继承 / 下限」判定

    诚实边界(残余攻击面,未在本条关闭):任何非 server 类型且被显式授予 Leader 的登记记录,仍可被「报它的名字」继承。现网该类记录 0 条(agents_register 只被 server 内部调用:启动写 1 条 server/Leader,claim·execute 写 Executor),且 MCP 工具面 没有 agents_register ⇒ 要利用需先有内部写入面。彻底关闭需认证身份(BUG-422 A 案 / C 案)。

    SqliteStore::agents_set_pid

    fn SqliteStore::agents_set_pid(self : SqliteStore, agent_id~ : String, pid~ : Int) -> Result[Unit, String]

    换绑进程实例(server 启动时调用):仅刷新 pid,不动 first_seen/last_seen。

    SqliteStore::agents_set_pubkey

    fn SqliteStore::agents_set_pubkey(self : SqliteStore, agent_id~ : String, pubkey_fp~ : String) -> Result[Unit, String]

    BUG-430 / S3 第八段:改写(或清空)某 agent 已登记的公钥。

    为什么必须新增这条写入面(三条互证,实测): ① agents_register 的 ON CONFLICT(agent_id) DO UPDATE SET 不含 pubkey_fp (只更新 pid / last_seen / status / role)⇒ 已存在的公钥永远不会被改; ② 本文件全仓 UPDATE agents SET 此前只有 agents_set_pid 一处; ③ agents_* 十个函数里没有 agents_set_pubkey。 ⇒ 公钥一旦写入即永久不可变:登记错了(含被抢注)该身份就永久废掉。

    本函数补上「可改」这条能力,供两条路使用: · 轮换:agents_set_pubkey(fp, 新公钥)(调用方须先验过旧私钥签名) · 吊销:agents_set_pubkey(fp, "")(清空 ⇒ 之后传 challenge 恒 challenge_failed)

    ⚠️ 本函数只写库,不做任何鉴权 —— 「该不该换」由上层判定面 (key_rotate_verdict / key_revoke_verdict)与工具受理; 把鉴权塞进 store 会让同一规则出现两份实现(判例-29)。

    ⚠️ 传空串是有意的合法用法(吊销),不当作「参数漏传」拒绝。

    SqliteStore::agents_stale_check

    fn SqliteStore::agents_stale_check(self : SqliteStore, cutoff : String) -> Array[AgentRecord]

    陈旧 agent 扫描:返回 last_seen 早于 cutoff(或从未心跳)的 agent 列表。 cutoff 与 last_seen 同为 YYYY-MM-DDTHH:MM:SSZ,字典序比较成立。

    SqliteStore::archive_task

    fn SqliteStore::archive_task(self : SqliteStore, t :
    Task
    , archived_at~ : String) -> Result[Unit, String]

    将已归档任务写入 archive 表快照。

    SqliteStore::assistant_get

    fn SqliteStore::assistant_get(self : SqliteStore, agent : String) -> AssistantReg?

    SQLite 后端:查询某 agent 的助理登记(不存在返回 None)。

    SqliteStore::assistant_list

    fn SqliteStore::assistant_list(self : SqliteStore) -> Array[AssistantReg]

    SQLite 后端:列出全部助理登记。

    SqliteStore::assistant_set_status

    fn SqliteStore::assistant_set_status(self : SqliteStore, agent : String, status : String) -> Bool

    SQLite 后端:改某 agent 登记的 status(不存在返回 false)。

    SqliteStore::assistant_upsert

    fn SqliteStore::assistant_upsert(self : SqliteStore, reg : AssistantReg) -> Unit

    SQLite 后端:写入/覆盖一个助理登记(幂等)。

    SqliteStore::cb_get

    fn SqliteStore::cb_get(self : SqliteStore, ns : String, circuit : String) -> (String, Int, Int, Int, Int, Int, Int)?

    读取某熔断器记录(R104):(state, failures, window_secs, threshold, recovery_secs, opened_at, last_fail_ts)。

    SqliteStore::cb_save

    fn SqliteStore::cb_save(self : SqliteStore, ns : String, circuit : String, state : String, failures : Int, window_secs : Int, threshold : Int, recovery_secs : Int, opened_at : Int, last_fail_ts : Int, updated_at : String) -> Result[Unit, String]

    写入/覆盖熔断器记录(R104,ON CONFLICT DO UPDATE 幂等)。

    SqliteStore::challenge_cleanup

    fn SqliteStore::challenge_cleanup(self : SqliteStore, cutoff : String) -> Int

    清理:删「已消费」或「签发时间早于 cutoff」的行。

    为什么用 ISO 字符串直接比:本仓时间戳一律 YYYY-MM-DDTHH:MM:SSZ(定宽)⇒ 字典序 == 时间序 (与 assistant_claim_action 的 cutoff 同口径)。

    返回删除行数。未消费但已过 cutoff 的票也删 —— 它已过期,留着只会让表无限增长。

    SqliteStore::challenge_cleanup_aggressive

    fn SqliteStore::challenge_cleanup_aggressive(self : SqliteStore, cutoff : String) -> Int

    激进档:额外删掉已消费的行(审计证据一并丢失)。

    只在表确实需要压缩时用。⚠️ 用这一档要接受「查不到历史票的使用记录」。

    SqliteStore::challenge_cleanup_conservative

    fn SqliteStore::challenge_cleanup_conservative(self : SqliteStore, cutoff : String) -> Int

    保守档(默认):删「签发早于 cutoff」的行,不管有没有消费。

    保留 cutoff 之内的全部审计证据;表仍受「签发时间上界」约束而不会无限增长。

    SqliteStore::challenge_consume

    fn SqliteStore::challenge_consume(self : SqliteStore, ns : String, agent_fp : String, nonce : String) -> Bool

    原子一次性消费:把未消费的那张票标成已消费。

    返回 true ⇔ 本次调用把它从 consumed=0 改成 1(并发下只有一个调用能看到 true)。 返回 false ⇒ 票不存在 / 已被消费 / 不属于该 agent —— 三种情况都按重放处理(fail-closed)。

    判据是 Database::changes()(受影响行数),不是先 SELECT 再 UPDATE (那是先读后写,两个并发请求会都读到 0 然后都更新成功 ⇒ 一次性失效)。

    SqliteStore::challenge_consumed_count

    fn SqliteStore::challenge_consumed_count(self : SqliteStore) -> Int

    只读诊断:已消费的票数。

    SqliteStore::challenge_issue

    fn SqliteStore::challenge_issue(self : SqliteStore, ns : String, agent_fp : String, nonce : String, issued_at : String) -> Result[Unit, String]

    签发一张 challenge(落库)。INSERT OR IGNORE ⇒ 重复签发同 nonce 幂等。

    SqliteStore::challenge_pending_count

    fn SqliteStore::challenge_pending_count(self : SqliteStore, ns : String, agent_fp : String) -> Int

    只读诊断:某 agent 在 ns 下未消费的票数(供白盒锁断言「签发后 +1、消费后 -1」)。

    SqliteStore::challenge_total_count

    fn SqliteStore::challenge_total_count(self : SqliteStore) -> Int

    只读诊断:表总行数(让「表在长」可见)。供回执与白盒锁用。

    SqliteStore::clear

    fn SqliteStore::clear(self : SqliteStore) -> Unit

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::close

    fn SqliteStore::close(self : SqliteStore) -> Unit

    关闭底层连接(幂等) —— BUG-463。

    为什么需要它:@sqlite.Database 持有操作系统级文件句柄。Windows 上未关闭的 sqlite 句柄会一直握着 .db(以及 -wal/-shm)的文件锁 ⇒ ① 测试夹具里删不掉库文件、 ② 同路径重新 open 可能撞 database is locked、③ MultiStore::close_all 号称 「关闭全部命名空间」却只把 store 从 Map 里摘掉,句柄永远不会释放(进程内泄漏, 直到进程退出才由 OS 回收)。本函数把「关闭」这件事真正落到句柄上。

    幂等语义(两种面都要,别只做第一种):
    1. 自己重复调 close() ⇒ no-op(db 已是 None),不对已释放对象二次 close (js 侧是用已关闭句柄,native 侧是 double free 面);
    2. 底层句柄已被外部先关掉(白盒测试直接 match s.db { Some(db) => db.close() } 是合法写法)⇒ 本函数仍必须安全返回:关闭动作包了容错,吞掉 node:sqlite 的 database is not open,并把状态统一归零。实测反例:BUG-463 首版只做第一种, t744_9 白盒直关 + close_all 二次关闭 ⇒ 整个测试进程未捕获异常、汇总行都没了。

    对外契约:close() 恒成功、可重复调用;关闭后既有操作走各自 None 分支。

    关闭后的既有操作:全部走各函数既有的 None 分支 —— 返回 Err("sqlite store 未打开")(写路径)/ 空数组 / false / (), 不 panic。白盒锁见 store_bug463_close_wbtest.mbt。

    SqliteStore::create_task

    fn SqliteStore::create_task(self : SqliteStore, task :
    Task
    ) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::db_path_from_env

    fn SqliteStore::db_path_from_env(env_value : String?) -> String

    由环境值决定库路径(纯函数,便于白盒判据;空串视为未设置)。

    SqliteStore::default_db_path

    fn SqliteStore::default_db_path() -> String

    进程默认库路径回显(只读,供工具与 CLI 说明"这一轮的行到底落在哪个文件")。

    SqliteStore::delete_heartbeat

    fn SqliteStore::delete_heartbeat(self : SqliteStore, task_id : String) -> Result[Unit, String]

    删除心跳记录。

    SqliteStore::delete_task

    fn SqliteStore::delete_task(self : SqliteStore, id : String) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::ensure_specs_table

    fn SqliteStore::ensure_specs_table(self : SqliteStore) -> Bool

    确保 specs 表存在(幂等;旧库自动补齐)。

    SqliteStore::evolve_bump_child

    fn SqliteStore::evolve_bump_child(self : SqliteStore, id : String) -> Bool

    递增某产物的 children 计数(每次新增子代 +1)。返回是否成功。

    SqliteStore::evolve_list

    fn SqliteStore::evolve_list(self : SqliteStore) -> Array[Json]

    读取全部 DGM 产物(evolve_artifacts 表),按 created_at 升序。 返回 JSON 数组(供 MCP/展示)。

    SqliteStore::evolve_upsert

    fn SqliteStore::evolve_upsert(self : SqliteStore, id~ : String, parent_id~ : String, goal~ : String, note~ : String, code~ : String, score~ : Double, parts_json~ : String, created_at~ : String) -> Result[Unit, String]

    插入一个 DGM 产物(evolve_artifacts 表)。失败返回 Err。

    SqliteStore::executor_clear

    fn SqliteStore::executor_clear(self : SqliteStore) -> Bool

    SQLite 后端:清空全部执行者能力注册(Marketplace 重置/整洁)。

    SqliteStore::executor_list

    fn SqliteStore::executor_list(self : SqliteStore) -> Array[(String, String, String)]

    SQLite 后端:列出全部执行者 (name, abilities_json, created_at)。

    SqliteStore::executor_save

    fn SqliteStore::executor_save(self : SqliteStore, name~ : String, abilities_json~ : String, created_at~ : String) -> Result[String, String]

    SQLite 后端:写入/覆盖一个执行者的能力注册。(幂等:同 name 覆盖)

    SqliteStore::get_spec

    fn SqliteStore::get_spec(self : SqliteStore, id : String) -> SpecRecord?

    按 id 读取单条语料记录。

    SqliteStore::get_task

    fn SqliteStore::get_task(self : SqliteStore, id : String) ->
    Task
    ?

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::list_all_heartbeats

    fn SqliteStore::list_all_heartbeats(self : SqliteStore) -> Array[(String, String, String, String)]

    列出全部心跳记录(供启动时加载)。

    SqliteStore::list_archived

    fn SqliteStore::list_archived(self : SqliteStore) -> Array[(String, String)]

    列出全部归档快照(id -> 原 task_json)。

    SqliteStore::list_heartbeat_intervals

    fn SqliteStore::list_heartbeat_intervals(self : SqliteStore, task_id : String, limit? : Int) -> Array[Int]

    读取指定任务的心跳间隔历史(时间序:旧→新;最多 limit 条,默认 100)。

    SqliteStore::list_saga_actions

    fn SqliteStore::list_saga_actions(self : SqliteStore, ns : String, root_task_id : String) -> Array[(Int, String, String, String, String)]

    列出某根任务已登记的 Saga 补偿步骤(前向顺序 id ASC:登记顺序)。 返回 (id, step, task_id, compensation, status) 五元组数组。

    SqliteStore::list_specs_by_task

    fn SqliteStore::list_specs_by_task(self : SqliteStore, task_id : String) -> Array[SpecRecord]

    列出某任务的全部语料 / 复验记录(按 created_at、id 升序)。

    SqliteStore::list_tasks

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::list_tasks_in

    fn SqliteStore::list_tasks_in(self : SqliteStore, ns_filter : String) -> Array[
    Task
    ]

    列出指定命名空间的任务。 BUG-85:空 ns 的回退口径与内存后端对齐(list_tasks_in("") = 默认命名空间), 否则同一个入参在两个后端上一个是 default 行、一个是零行。

    SqliteStore::lock_violations_list

    fn SqliteStore::lock_violations_list(self : SqliteStore) -> Array[LockViolation]

    SQLite 后端:列出违例记录。

    SqliteStore::lock_violations_push

    fn SqliteStore::lock_violations_push(self : SqliteStore, v : LockViolation) -> Unit

    SQLite 后端:追加一条违例记录。

    SqliteStore::locks_get

    fn SqliteStore::locks_get(self : SqliteStore, scope : String) -> FileLock?

    SQLite 后端:查询某 scope 的锁(不存在返回 None)。

    SqliteStore::locks_list

    fn SqliteStore::locks_list(self : SqliteStore) -> Array[FileLock]

    SQLite 后端:列出全部锁。

    SqliteStore::locks_release

    fn SqliteStore::locks_release(self : SqliteStore, scope : String, agent : String) -> Bool

    SQLite 后端:释放某 scope 的锁(仅持有者可释放)。

    SqliteStore::locks_sweep

    fn SqliteStore::locks_sweep(self : SqliteStore, now : String) -> Int

    SQLite 后端:清理过期锁(active 且 ttl_until < now → expired + 记违例)。

    SqliteStore::locks_try_acquire

    fn SqliteStore::locks_try_acquire(self : SqliteStore, lock : FileLock) -> LockAcquireOutcome

    SQLite 后端:按裁决抢锁(BUG-145)——抢不到就是 0 行,不是覆盖(BUG-136 同形)。 与 rsv_try_set 同一形状,三处要点逐字对齐:
    1. 互斥由数据库在 ON CONFLICT ... WHERE 里判,不由调用方写前那次读判 (「先读后写」的竞态窗口正是缺陷本体:两进程都读到空闲、都以为拿到了锁);
    2. 刻意不留「写前已见活体持有者就不发这笔写」的短路——那等于把裁决权交回 调用侧那次旧读,而且 SQL 的 WHERE 永远不会被执行(照 BUG-136 的同族教训);
    3. 结论一律取写后回读:库里现在是谁,回执就是谁。 WHERE 编码的是 lock_acquire_action→rsv_action 同一条规则: agent = excluded.agent(同持有者续,排在过期之前) OR status <> 'active'(复用一条已释放/已扫出的行——预订表没有这一维,锁表有) OR ttl_until < excluded.ts(旧主 TTL 已过,可让渡)

    SqliteStore::locks_upsert

    fn SqliteStore::locks_upsert(self : SqliteStore, lock : FileLock) -> Unit

    SQLite 后端:upsert 一把锁(按 scope 主键;旧锁 active 且非本人且未过期 → 记违例)。

    SqliteStore::mark_saga_actions_done

    fn SqliteStore::mark_saga_actions_done(self : SqliteStore, ns? : String, root_task_id~ : String, ids~ : Array[Int]) -> Result[Unit, String]

    把指定 Saga 补偿步骤标记为 done(已补偿/已消费),幂等。

    SqliteStore::new

    fn SqliteStore::new() -> SqliteStore?

    以默认路径打开的便捷构造。 BUG-90:默认路径过去写死 fist-mbt.db,调用面没有任何合规出口把一整轮验证 收进独立库 —— store_open(scratch=true) 自称"库落 temp/ 临时区,不污染仓库根", 实测任务行仍落仓库根 fist-mbt.db(工具闭包用的是模块级 engine,而非 mstore 里的 ns 库)。 这里补的是运维侧出口:FIST_DB_PATH 环境变量,与 FIST_RUN_CHECK_ALLOW 同族 (扩权位只在进程环境里,调用方传参不能自我扩权)。未设置时行为逐字不变。

    SqliteStore::open

    fn SqliteStore::open(db_path : String) -> SqliteStore?

    打开数据库并初始化五表 schema。失败返回 None(调用方决定是否 abort / 回退内存)。

    SqliteStore::open_preflight_ok

    fn SqliteStore::open_preflight_ok(db_path : String) -> Bool

    打开前的可写性预检:把「SqliteStore::open 失败返回 None」这句契约在 js 侧也变成真的。

    为什么还要这一层(BUG-390 的遗留项 ①,上面那段注释点名了却一直没补):ensure_db_parent_dir 只会建目录,而且失败被 catch { _ => () } 吞掉——目录建不出来时它照样往下走。而 js 侧 daqing/sqlite 的 Database::open 是 Some(new DatabaseSync(path)) 无条件、从不返回 null, 只会 throw(.mooncakes/daqing/sqlite/sqlite_js.mbt)⇒ 任何开库失败都以未捕获异常终止 serve 进程,调用面(store_open / engine_for_ns)永远接不到 false/None:一个畸形 namespace/data_dir 参数就是一次服务面 DoS。native 侧同一个函数诚实返回 None ⇒ 这条契约 只在 native 为真,正是「同族守卫不许两套规则」(BUG-78 口径)该收口的形状。

    建不出目录的 reachable 形态(都过 data_dir_ok,无需绝对路径或穿越): ① 父段是既存文件(data_dir="README.md/sub");② 目标名撞既存目录(存在 ghost.db/ 时 ns=ghost);③ Windows 保留设备名(aux / con / nul / com1,aux.db 整段保留); ④ 权限不足。

    预检口径:既存目标必须是普通文件;不存在则用一次「写空文件」探测父目录可写性与名字合法性—— SQLite 本来就要创建这个文件,探测成功即等价于把它的活提前干完,不新增残留类别。 :memory: 与空串不涉文件系统,直接放行。

    SqliteStore::read_heartbeat

    fn SqliteStore::read_heartbeat(self : SqliteStore, task_id : String) -> (String, String, String)

    读取指定任务的心跳记录。

    SqliteStore::recent_call_logs

    fn SqliteStore::recent_call_logs(self : SqliteStore, limit? : Int) -> Array[Json]

    读取最近 N 条调用日志(按自增 id DESC,新的在前;seq 仅作同进程单调定位)。

    SqliteStore::record_execution

    fn SqliteStore::record_execution(self : SqliteStore, task_id~ : String, executor~ : String, model~ : String, tokens_in~ : Int, tokens_out~ : Int, cost~ : Double, duration_ms~ : Int, rate_limited~ : Bool, failure_reason~ : String, created_at~ : String) -> Result[Unit, String]

    写入执行记录(供 execute 工具调用)。

    SqliteStore::rsv_get

    fn SqliteStore::rsv_get(self : SqliteStore, scope : String) -> (String, String, String)?

    SQLite 后端:查询某作用域预订。

    SqliteStore::rsv_list

    fn SqliteStore::rsv_list(self : SqliteStore) -> Array[(String, String, String)]

    SQLite 后端:列出全部预订。

    SqliteStore::rsv_release

    fn SqliteStore::rsv_release(self : SqliteStore, scope : String, agent : String) -> Bool

    SQLite 后端:释放预订(仅持有者)。 BUG-79:Statement::execute -> Bool 只表示「语句没报错」——绑定层(mizchi/sqlite 的 js 与 native 两版签名都是 execute -> Bool)根本不给 changes(),删 0 行同样返回 true。 于是非持有者调 reserve_release 会被告知「已释放」而预订仍在,两个 agent 同改一份作用域, 正是这个原语要防的多 agent 撞车;内存后端校持有者,所以它的单测永远绿、抓不到。 修法:删前先按 scope 读回持有者比对,不匹配即 false(与内存后端同语义)。

    SqliteStore::rsv_set

    fn SqliteStore::rsv_set(self : SqliteStore, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[String, String]

    SQLite 后端:写入/覆盖某作用域预订(ON CONFLICT DO UPDATE)。

    SqliteStore::rsv_try_set

    fn SqliteStore::rsv_try_set(self : SqliteStore, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[RsvOutcome, String]

    SQLite 后端:按裁决写预订——抢不到就是 0 行,不是异常(BUG-136)。 与旧 rsv_set(无条件 ON CONFLICT DO UPDATE SET)的区别在 WHERE: 只有「本来就是我的」或「现持有者的 TTL 已过期」才覆盖,二者都由数据库判, 不由调用方写前那次读判(excluded.created_at 就是本次请求的服务端盖章时刻)。 纯 INSERT 不带 conflict 子句这条路不能用:约束冲突会撞上面那个抛异常形态, 把一次正常冲突变成 server 崩溃。结论一律取写后回读,与内存后端同构。

    SqliteStore::update_spec_status

    fn SqliteStore::update_spec_status(self : SqliteStore, id : String, status : String, content : String, updated_at : String) -> Result[Unit, String]

    更新语料状态(可同时更新内容与时间戳)。

    SqliteStore::update_task

    fn SqliteStore::update_task(self : SqliteStore, task :
    Task
    ) -> Result[Unit, String]

    提升声明:允许通过具体类型直接调用 trait 方法。

    SqliteStore::update_task_if_status

    fn SqliteStore::update_task_if_status(self : SqliteStore, task :
    Task
    , expected_status : String) -> Result[Unit, String]

    SqliteStore::upsert_spec

    fn SqliteStore::upsert_spec(self : SqliteStore, rec : SpecRecord) -> Result[Unit, String]

    写入 / 覆盖一条语料记录(UPSERT,主键 id)。

    SqliteStore::write_call_log

    fn SqliteStore::write_call_log(self : SqliteStore, ts~ : String, tool~ : String, caller~ : String, ns~ : String, params_json~ : String, result_json~ : String, ok~ : Bool, runtime_ms~ : Int, seq~ : Int) -> Result[Unit, String]

    写入一条调用日志到 call_log 表。best-effort:失败返回 Err(调用方应忽略)。

    SqliteStore::write_heartbeat

    fn SqliteStore::write_heartbeat(self : SqliteStore, agent_id~ : String, task_id~ : String, last_seen~ : String, status~ : String) -> Result[Unit, String]

    写入心跳记录(UPSERT:存在则更新 last_seen / status,不存在则插入)。

    SqliteStore::write_heartbeat_interval

    fn SqliteStore::write_heartbeat_interval(self : SqliteStore, task_id~ : String, interval_sec~ : Int, ts~ : String) -> Result[Unit, String]

    追加一条心跳间隔历史(R90,Phi Accrual 判活的数据源);同一任务保留最近 1000 条。

    SqliteStore::write_saga_action

    fn SqliteStore::write_saga_action(self : SqliteStore, ns? : String, root_task_id~ : String, step~ : String, task_id? : String, compensation~ : String, created_at~ : String) -> Result[Unit, String]

    登记一条 Saga 补偿步骤(R92,durable action log):同一 (ns, root_task_id, step) 幂等覆盖——重登记会把 status 重置为 pending(重试安全),created_at 刷新。

    StoreBackend

    pub enum StoreBackend {
    Mem(MemoryStore)
    Sql(SqliteStore)
    }

    后端路由:内存版(默认,测试/演示)或 SQLite 持久化(运行时内建数据库)。 FistEngine 持此类型,业务层不感知具体后端。

    StoreBackend::assistant_get

    fn StoreBackend::assistant_get(self : StoreBackend, agent : String) -> AssistantReg?

    StoreBackend::assistant_list

    fn StoreBackend::assistant_list(self : StoreBackend) -> Array[AssistantReg]

    StoreBackend::assistant_set_status

    fn StoreBackend::assistant_set_status(self : StoreBackend, agent : String, status : String) -> Bool

    StoreBackend::assistant_upsert

    fn StoreBackend::assistant_upsert(self : StoreBackend, reg : AssistantReg) -> Unit

    StoreBackend::backend_name

    fn StoreBackend::backend_name(self : StoreBackend) -> String

    后端自述(BUG-119):"sqlite" 表示心跳真的落库、跨进程可读;"memory" 表示只活在本进程。 看护面必须把它打进回执——否则内存后端会冒充"心跳已持久化"。

    StoreBackend::cb_get

    fn StoreBackend::cb_get(self : StoreBackend, ns : String, circuit : String) -> (String, Int, Int, Int, Int, Int, Int)?

    读取某熔断器记录(R104)。BUG-439:内存后端已实读(此前恒 None ⇒ 永不熔断)。

    返回 (state, failures, window_secs, threshold, recovery_secs, opened_at, last_fail_ts), 与 SqliteStore::cb_get 逐字段同序。

    StoreBackend::cb_save

    fn StoreBackend::cb_save(self : StoreBackend, ns : String, circuit : String, state : String, failures : Int, window_secs : Int, threshold : Int, recovery_secs : Int, opened_at : Int, last_fail_ts : Int, updated_at : String) -> Result[Unit, String]

    写入/覆盖熔断器记录(R104)。BUG-439:内存后端已实写(此前是 no-op ⇒ 计数从不累积)。

    语义与 SQLite 的 ON CONFLICT(ns, circuit) DO UPDATE 逐字段一致: 整体覆盖,不保留旧值(引擎侧每次 save 都写全量快照)。

    StoreBackend::challenge_cleanup

    fn StoreBackend::challenge_cleanup(self : StoreBackend, cutoff : String) -> Int

    清理过期/已消费的 challenge,返回删除行数。

    StoreBackend::challenge_cleanup_aggressive

    fn StoreBackend::challenge_cleanup_aggressive(self : StoreBackend, cutoff : String) -> Int

    BUG-467:激进档清理 —— 额外删已消费的行(审计证据一并丢失)。

    StoreBackend::challenge_cleanup_conservative

    fn StoreBackend::challenge_cleanup_conservative(self : StoreBackend, cutoff : String) -> Int

    BUG-467:保守档清理(默认推荐) —— 只删「签发早于 cutoff」的行,不管消费与否。

    与激进档 challenge_cleanup 的差别:这一档保留 cutoff 之内的审计证据 (consumed=1 是「这张票被用过」的唯一记录)。表仍受「签发时间上界」约束不会无限长。

    StoreBackend::challenge_consume

    fn StoreBackend::challenge_consume(self : StoreBackend, ns : String, agent_fp : String, nonce : String) -> Bool

    原子一次性消费(两个后端同语义:返回 true ⇔ 本次调用把它从「未消费」变成「已消费」)。

    StoreBackend::challenge_consumed_count

    fn StoreBackend::challenge_consumed_count(self : StoreBackend) -> Int

    BUG-467:已消费的票数。

    StoreBackend::challenge_issue

    fn StoreBackend::challenge_issue(self : StoreBackend, ns : String, agent_fp : String, nonce : String, issued_at : String) -> Result[Unit, String]

    签发一张 challenge(两个后端都真写)。

    StoreBackend::challenge_pending_count

    fn StoreBackend::challenge_pending_count(self : StoreBackend, ns : String, agent_fp : String) -> Int

    只读诊断:某 agent 在 ns 下未消费的票数。

    StoreBackend::challenge_persisted

    fn StoreBackend::challenge_persisted(self : StoreBackend) -> Bool

    后端是否真正持久化了 challenge(true = SQLite 落库、跨进程可见; false = 内存后端,只在本次进程生命周期内有效)。

    StoreBackend::challenge_total_count

    fn StoreBackend::challenge_total_count(self : StoreBackend) -> Int

    BUG-467:表总行数(让「表在长」可观测)。

    StoreBackend::clear

    fn StoreBackend::clear(self : StoreBackend) -> Unit

    StoreBackend::cost_stats

    fn StoreBackend::cost_stats(self : StoreBackend) -> Json

    成本聚合统计:SQLite 后端从 executions 表聚合,内存后端返回零值。

    StoreBackend::create_task

    fn StoreBackend::create_task(self : StoreBackend, task :
    Task
    ) -> Result[Unit, String]

    StoreBackend::delete_heartbeat

    fn StoreBackend::delete_heartbeat(self : StoreBackend, task_id : String) -> Result[Unit, String]

    删除心跳记录。

    StoreBackend::delete_task

    fn StoreBackend::delete_task(self : StoreBackend, id : String) -> Result[Unit, String]

    StoreBackend::executor_clear

    fn StoreBackend::executor_clear(self : StoreBackend) -> Bool

    后端统一:清空全部执行者能力注册(Marketplace 重置/整洁)。

    StoreBackend::executor_list

    fn StoreBackend::executor_list(self : StoreBackend) -> Array[(String, String, String)]

    后端统一:列出全部执行者 (name, abilities_json, created_at)。

    StoreBackend::executor_save

    fn StoreBackend::executor_save(self : StoreBackend, name~ : String, abilities_json~ : String, created_at~ : String) -> Result[String, String]

    后端统一:写入/覆盖一个执行者的能力注册(幂等)。

    StoreBackend::get_spec

    fn StoreBackend::get_spec(self : StoreBackend, id : String) -> SpecRecord?

    按 id 读取单条语料记录。

    StoreBackend::get_task

    StoreBackend::list_all_heartbeats

    fn StoreBackend::list_all_heartbeats(self : StoreBackend) -> Array[(String, String, String, String)]

    列出全部心跳记录(供启动时加载)。

    StoreBackend::list_heartbeat_intervals

    fn StoreBackend::list_heartbeat_intervals(self : StoreBackend, task_id : String, limit? : Int) -> Array[Int]

    读取心跳间隔历史(R90:内存后端返回空)。

    StoreBackend::list_saga_actions

    fn StoreBackend::list_saga_actions(self : StoreBackend, ns : String, root_task_id : String) -> Array[(Int, String, String, String, String)]

    列出某根任务的 Saga 补偿步骤(R92:前向顺序 = 登记顺序)。 BUG-441:内存后端已实读(此前恒 [] ⇒ "没有待补偿步骤"永远成立)。

    返回 (id, step, task_id, compensation, status) 五元组数组,与 SQLite 同序同形。

    StoreBackend::list_specs_by_task

    fn StoreBackend::list_specs_by_task(self : StoreBackend, task_id : String) -> Array[SpecRecord]

    列出某任务的全部语料 / 复验记录。

    StoreBackend::list_tasks

    StoreBackend::list_tasks_in

    fn StoreBackend::list_tasks_in(self : StoreBackend, ns : String) -> Array[
    Task
    ]

    列出指定命名空间的任务(跨后端)

    StoreBackend::lock_violations_list

    fn StoreBackend::lock_violations_list(self : StoreBackend) -> Array[LockViolation]

    StoreBackend::lock_violations_push

    fn StoreBackend::lock_violations_push(self : StoreBackend, v : LockViolation) -> Unit

    StoreBackend::locks_get

    fn StoreBackend::locks_get(self : StoreBackend, scope : String) -> FileLock?

    StoreBackend::locks_list

    fn StoreBackend::locks_list(self : StoreBackend) -> Array[FileLock]

    StoreBackend::locks_release

    fn StoreBackend::locks_release(self : StoreBackend, scope : String, agent : String) -> Bool

    StoreBackend::locks_sweep

    fn StoreBackend::locks_sweep(self : StoreBackend, now : String) -> Int

    StoreBackend::locks_try_acquire

    fn StoreBackend::locks_try_acquire(self : StoreBackend, lock : FileLock) -> LockAcquireOutcome

    后端统一:按裁决抢锁(BUG-145)——被他人 live 持有时拒绝,不覆盖。 两后端必须同一形状(先读后写只在单进程测不出竞态,BUG-85/79 的旧例)。

    StoreBackend::locks_upsert

    fn StoreBackend::locks_upsert(self : StoreBackend, lock : FileLock) -> Unit

    StoreBackend::mark_saga_actions_done

    fn StoreBackend::mark_saga_actions_done(self : StoreBackend, ns? : String, root_task_id~ : String, ids~ : Array[Int]) -> Result[Unit, String]

    把指定 Saga 补偿步骤标记 done(R92:幂等)。 BUG-441:内存后端已实操作(此前 Ok(()) 静默无操作 ⇒ mark 了但列表不变)。

    StoreBackend::memory

    fn StoreBackend::memory() -> StoreBackend

    后端工厂(跨包构造 StoreBackend 需经 pub 函数)

    StoreBackend::read_heartbeat

    fn StoreBackend::read_heartbeat(self : StoreBackend, task_id : String) -> (String, String, String)

    读取心跳记录(返回 (agent_id, last_seen, status),无记录返回 ("", "", ""))。

    StoreBackend::recent_call_logs

    fn StoreBackend::recent_call_logs(self : StoreBackend, limit? : Int) -> Array[Json]

    读取最近 N 条调用日志(双后端统一入口,新的在前)。

    StoreBackend::record_execution

    fn StoreBackend::record_execution(self : StoreBackend, task_id~ : String, executor~ : String, model~ : String, tokens_in~ : Int, tokens_out~ : Int, cost~ : Double, duration_ms~ : Int, rate_limited~ : Bool, failure_reason~ : String, created_at~ : String) -> Result[Unit, String]

    执行元数据记录(可选参数,供 execute 工具写入 executions 表)。 MemoryStore 为 no-op,SqliteStore 写入 executions 表。

    StoreBackend::rsv_get

    fn StoreBackend::rsv_get(self : StoreBackend, scope : String) -> (String, String, String)?

    后端统一:查询某作用域预订。

    StoreBackend::rsv_list

    fn StoreBackend::rsv_list(self : StoreBackend) -> Array[(String, String, String)]

    后端统一:列出全部预订。

    StoreBackend::rsv_release

    fn StoreBackend::rsv_release(self : StoreBackend, scope : String, agent : String) -> Bool

    后端统一:释放预订(仅持有者)。

    StoreBackend::rsv_set

    fn StoreBackend::rsv_set(self : StoreBackend, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[String, String]

    后端统一:写入/覆盖某作用域预订(拿来主义:Interlinked 文件预订)。

    StoreBackend::rsv_try_set

    fn StoreBackend::rsv_try_set(self : StoreBackend, scope~ : String, agent~ : String, ttl_until~ : String, created_at~ : String) -> Result[RsvOutcome, String]

    后端统一:按裁决写预订(BUG-136)。抢不到就抢不到,绝不覆盖别人的活体预订; 两个后端返回同一种结论(旧例 BUG-85:同名方法两种语义,单测永远绿而线上红)。

    StoreBackend::set_namespace

    fn StoreBackend::set_namespace(self : StoreBackend, ns : String) -> Unit

    设置默认命名空间

    StoreBackend::sqlite

    StoreBackend::sqlite_store

    fn StoreBackend::sqlite_store(self : StoreBackend) -> SqliteStore?

    提取底层 SQLite 句柄(内存后端返回 None)。 供跨包复用 SqliteStore 专有能力(如 evolve_artifacts 的 evolve_upsert / evolve_list)。

    StoreBackend::update_spec_status

    fn StoreBackend::update_spec_status(self : StoreBackend, id : String, status : String, content : String, updated_at : String) -> Result[Unit, String]

    更新语料状态。

    StoreBackend::update_task

    fn StoreBackend::update_task(self : StoreBackend, task :
    Task
    ) -> Result[Unit, String]

    StoreBackend::update_task_if_status

    fn StoreBackend::update_task_if_status(self : StoreBackend, task :
    Task
    , expected_status : String) -> Result[Unit, String]

    StoreBackend::upsert_spec

    fn StoreBackend::upsert_spec(self : StoreBackend, rec : SpecRecord) -> Result[Unit, String]

    写入 / 覆盖一条语料记录(内存与 SQLite 后端均真实落库)。

    StoreBackend::write_call_log

    fn StoreBackend::write_call_log(self : StoreBackend, ts~ : String, tool~ : String, caller~ : String, ns~ : String, params_json~ : String, result_json~ : String, ok~ : Bool, runtime_ms~ : Int, seq~ : Int) -> Result[Unit, String]

    写入一条调用日志(双后端统一入口,best-effort:失败仅返回 Err,调用方应忽略)。

    StoreBackend::write_heartbeat

    fn StoreBackend::write_heartbeat(self : StoreBackend, agent_id~ : String, task_id~ : String, last_seen~ : String, status~ : String) -> Result[Unit, String]

    写入心跳记录(SQLite 落库,内存后端为 no-op)。

    StoreBackend::write_heartbeat_interval

    fn StoreBackend::write_heartbeat_interval(self : StoreBackend, task_id~ : String, interval_sec~ : Int, ts~ : String) -> Result[Unit, String]

    追加心跳间隔历史(R90:SQLite 落库,内存后端为 no-op——与心跳表同模式)。

    StoreBackend::write_saga_action

    fn StoreBackend::write_saga_action(self : StoreBackend, ns? : String, root_task_id~ : String, step~ : String, task_id? : String, compensation~ : String, created_at~ : String) -> Result[Unit, String]

    登记 Saga 补偿步骤(R92:SQLite 落库 durable action log)。 BUG-441:内存后端已实写(此前 Ok(()) no-op ⇒ 补偿序列静默为空)。

    语义与 SQLite 的 ON CONFLICT(ns, root_task_id, step) DO UPDATE 一致: 同一 (ns, root_task_id, step) 重复登记 ⇒ 就地更新(task_id/compensation 覆盖、 status 复位为 pending),不新增行、不改 id。

    StoreMigration

    pub struct StoreMigration {
    reason : String
    applied : Bool
    from : String
    to : String
    legacy_tasks : Int
    new_tasks : Int
    moved_tasks : Int
    backup : String
    note : String
    }

    迁移结论(回执的数据源)。字段全部是"这次 open 到底做了什么"的自证: 走哪个位置、有没有搬、搬了多少条、旧库/新库各多少条、备份落在哪。

    StoreMigration::skipped

    fn StoreMigration::skipped(reason : String) -> StoreMigration

    不迁移的结论工厂(显式 data_dir / ns 已打开 / 非法 ns 等旁路用同一形状回执, 免得调用方要在两种结构之间分支)。

    StoreMigration::skipped_at

    fn StoreMigration::skipped_at(reason : String, from : String, to : String) -> StoreMigration

    同上,但点名两边路径("为什么没迁"要能指到具体文件)。

    StoreMigration::to_json

    fn StoreMigration::to_json(self : StoreMigration) -> Json

    回执 JSON(store_open 的 migration 字段唯一真源)。

    agent_record_is_process_self

    fn agent_record_is_process_self(rec : AgentRecord) -> Bool

    这条记录是不是「本进程自己的身份」(BUG-422)。

    判据只看登记来源:agent_type == "server"(大小写归一)。 本仓唯一的写入点是 server.mbt 的 make_engine(): agent_touch_first_seen(eng, "fist-mbt", "default", "server", "Leader", …) —— 也就是 server 进程给「自己」登记的那一条。

    为什么需要这条判据:角色裁决的 declared 取值以调用方自报的名字为查表键, 而 fist-mbt 是 agents 表里唯一 role=Leader 的记录 ⇒ 报这个名字就继承 Leader (实测 ALLOWED agent=fist-mbt src/engine/* · src/core/*,BUG-422 本尊)。 服务器自己的身份不该被任何外部 agent 冒用,所以它整条退出角色裁决。

    这是事实判据,不是政策:政策(哪些角色可继承、推断是下限)在 engine_scope_perm.mbt 的 inheritable_role / resolve_agent_role。

    aggregate_stats

    fn aggregate_stats(db :
    Database
    ) -> Json

    聚合统计:总成本 / 总 token / 执行次数 / 按执行器分组

    assistant_claim_action

    fn assistant_claim_action(prev : AssistantReg?, agent : String, _now : String, cutoff : String) -> (Bool, String, String)

    助理选举裁决单一真源:给定现有 active 主助理与本次请求,判「能否当选」+动作标签。 两个后端共用(SQLite 侧语义一致)。cutoff 为心跳新鲜线(ISO 字符串,字典序比较)。 返回 (ok, action, prev_agent):action ∈ elected / renewed / taken_over / conflict。

    bug462_unique_db

    fn bug462_unique_db(tag : String) -> String

    生成一个每次调用都不同的 sqlite 夹具路径:temp/b462_<tag>_<token>.db。

    tag 只做可读性(事后在 temp/ 里认出是哪个测试留下的),不参与唯一性。

    bug462_unique_dir

    fn bug462_unique_dir(tag : String) -> String

    生成一个每次调用都不同的临时目录路径(用于需要真建/真删目录的测试)。

    与 bug462_unique_db 同源 token;差别只是没有 .db 后缀。 为什么需要它:multi_store_test.mbt 的 R28 补建目录锁会真建 temp/r28_xxx/ 并在收尾 remove_dir,中途清场——两进程并发时一个删了另一个正要用的目录。

    db_file_exists

    fn db_file_exists(path : String) -> Bool

    文件名探测:是普通文件则 true。

    @fs.is_file 在路径缺失时 raise(不是返回 false),迁移判定只关心"在不在", 所以这里一律折成 false——判定逻辑不该被一次 IO 异常打断。

    db_file_task_count

    fn db_file_task_count(path : String) -> Int

    只读任务计数(迁移判定与迁移后校验共用):打开某个库文件数 tasks 行数。

    不建 schema、不切 WAL、不补列——只读探针不该改动物证。对比之下 SqliteStore::open 会顺手 CREATE TABLE + PRAGMA journal_mode=WAL, 于是"探一下空壳"就把空壳变成了"有全套新表的新库",判定证据被自己改掉。

    三道闸(每一道都对应一个实测会打死进程或误判的形状):
    1. 文件不存在 → 0;
    2. 文件头不是 SQLite → 0(裸 JS 异常接不住,见 looks_like_sqlite);
    3. 先问 sqlite_master 有没有 tasks 表——任何合法库都有 sqlite_master, 而对缺表的库直接 SELECT ... FROM tasks 同样抛裸 JS 异常 (no such table: tasks,BUG-94 同族),接不住 ⇒ 空壳库必须先走这一步。

    executions_table_sql

    fn executions_table_sql() -> String

    executions 表 CREATE TABLE SQL(使用 \n 避免跨行字面量)

    init_executions_table

    fn init_executions_table(db :
    Database
    ) -> Bool

    初始化 executions 表

    insert_execution

    fn insert_execution(db :
    Database
    , rec : ExecutionRecord) -> Result[Unit, String]

    写入执行记录

    list_all_executions

    fn list_all_executions(db :
    Database
    ) -> Array[ExecutionRecord]

    列出全部执行记录(按 created_at DESC)

    lock_acquire_action

    fn lock_acquire_action(prev : FileLock?, agent : String, now : String) -> (Bool, String)

    锁层「该不该写」的裁决:逐字委托 rsv_action,不另写谓词。

    • 为什么必须有这一层(BUG-145 / 多 agent 锁机制报告 R24·R25):locks_upsert 原先 自带一段手写 forked 谓词,只决定「要不要追加一条 lock_taken_over 违例」, 随后无条件覆盖写——把 rsv_action 本应回的 (false, "conflict") 丢弃了。 后果是 lock_acquire 恒回 active:A 持 live 锁时 B 照样「拿到」同一把锁, 而设计文档 v3 的 lk2 明确要求「A 持锁后 B lock_acquire → granted=false, blocked_by=A」。
    • 单一真源纪律:冲突矩阵只写在 rsv_action 一处(同持有者排在过期之前那条亦然), 本函数只做形状归一,因此预订层与锁层不可能各自漂移。
    • now 传本次请求的时间戳(与旧 forked 谓词的 lock.ts 同一口径)。

    migrate_legacy_store

    fn migrate_legacy_store(legacy_dir : String, new_dir : String, ns : String, force? : Bool) -> StoreMigration

    旧位置 → BUG-78 新默认位置的真迁移(T0r744)。

    判定见 store_migration_reason:只有 legacy-only / empty-target / forced 会真的搬。搬迁口径(每一条都对应一个实测会丢数据的坑):
    1. 复制而不是移动:旧库原字节留在旧位置当备份,不删除、不改名—— 不可逆动作不自动做(本仓纪律),回执点名两边路径供人收尾;
    2. -wal 侧车一并复制(先试 checkpoint):只搬主库会丢最后一段事务;
    3. 目标位置的 -wal / -shm 先删:把新主库换掉却留着旧侧车, SQLite 会拿陈旧 WAL 帧覆盖新内容(换库场景的标准坑);
    4. forced(两边都有数据)覆盖前先把新库整份复制成 <new>.db.replaced-bak, 连同它的 -wal,不销毁新库内容;
    5. 落盘后做内容级校验(新库 tasks 行数 ≥ 旧库行数),不是只看文件存在—— "搬了个空壳/半截文件"必须判失败。

    ns_misroute_not_found

    fn ns_misroute_not_found(task_id : String, ns : String, main_ns : String) -> String

    BUG-420 查不到时的可行动文案(纯计算)。

    形状:带 ns 查不到,但主库里同 id 任务的 ns 标签正好等于请求的 ns。 这形状下 task not found: <id> 是误导性文案(看起来像 ID 写错), 必须指出「任务在主库、只是不在这个 ns 库」并给三条出路。

    前提由调用方(server 的 get/archive handler)负责核验:只在标签对得上时调用, 否则真正的「ID 不存在」会被说成路由错位(那是制造假诊断)。

    ns_routing_warning

    fn ns_routing_warning(ns : String, main_tasks : Int, store_tasks : Int) -> String

    BUG-420 路由错位诊断(纯计算)。

    main_tasks = 主库 fist-mbt.db 里带该 ns 标签的任务数; store_tasks = 该 ns 物理库(.fist-mbt/data/<ns>.db)里的任务数。

    仅当「主库有、本库空」且 ns 非 default/空时返回诊断文案,其余一律返回空串:
    • default/空串本来就走主库,不存在路由错位;
    • 分库已有数据说明写入路径工作正常,不必告警(不制造恒阳性)。

    rsv_action

    fn rsv_action(prev : (String, String, String)?, agent : String, now : String) -> (Bool, String)

    裁决规则单一真源:给定写前快照与本次请求,判「该不该写」+动作标签。 两个后端都从这里取标签,SQLite 侧的 ON CONFLICT ... WHERE 编码的是同一条规则 (reservations.agent = excluded.agent OR reservations.ttl_until < excluded.created_at)。 同持有者排在过期之前:自己续一条已过期的预订,报 renewed 而不是 taken_over(与旧口径逐字一致)。

    specs_table_sql

    fn specs_table_sql() -> String

    specs 表 CREATE TABLE SQL(幂等,兼容旧库)。

    store_migration_applies

    fn store_migration_applies(reason : String) -> Bool

    该判定是否真搬文件(只有这三档会写目标位置,其余一律只读判定)。

    store_migration_note

    fn store_migration_note(reason : String, legacy_path : String, new_path : String, legacy_tasks : Int, new_tasks : Int) -> String

    迁移判定的人话说明(回执 note 字段的唯一真源)。

    每一档都必须点出两边路径与行数——"做了没有/做了多少"要靠回执自证, 而不是让调用方去猜(T0r744 的第三项验收)。

    store_migration_reason

    fn store_migration_reason(legacy_present : Bool, legacy_tasks : Int, new_present : Bool, new_tasks : Int, force : Bool) -> String

    迁移判定(纯函数,零 IO):给定两边的存在性与任务数,判定这次 store_open 该不该搬库。

    返回闭集(每个取值都有对应文案,见 store_migration_note):
    • no-legacy 旧位置没有库(全新项目,正常落新位置)
    • legacy-only 只有旧库 → 搬(BUG-78 的正常升级路径)
    • empty-target 两边都有但新位置是空壳(0 条)且旧库有数据 → 搬(本次要治的形状)
    • both-empty 两边都有但都空 → 不搬(没有内容可搬)
    • target-has-data 旧库空壳、新位置有数据 → 不搬
    • already-migrated 新位置行数 ≥ 旧库 → 判为上次迁移的产物,不重复搬(幂等重入)
    • conflict 两边都有数据且新位置不覆盖旧库 → 不自动搬(自动覆盖会丢数据)
    • forced migrate=true 且旧库有数据 → 搬(新库先留档)
    • legacy-empty migrate=true 但旧库没有任务 → 不搬