异步之后的响应契约

更新于 · 在 sijie.xyz 查看原条目 ↗

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

副作用变异步后,响应不能撒谎:响应只承诺已经提交的事,还在路上的事给一个回执(事件 id / 任务 id),状态可查。需要“写完马上读到”的地方,用“在请求里等一小会儿,等不到就落回持久任务”。

每种副作用

副作用 响应里承诺什么 调用方怎么知道后续
搜索索引(写后马上搜) corpus.create / corpus.update / corpus.promote 最多等 2 秒,返回 indexed: true;超时返回 indexed: false + index_job_id MCP / UI 凭任务 id 调 tasks.get;AI 看到 false 就知道稍后再搜
访客申请通知 “已提交”,不说“已通知 owner” owner 在任务面板看到邮件任务状态;失败最终 discarded 会告警
批准邮件 码(同步签发)+ 邮件任务;MCP access_requests.approve 最多等 2 秒 申请行显示邮件状态:发送中、已发送或失败(completion-hooks)
预约通知 预约成功(预约行已提交),通知状态单列 同访客申请通知
Obsidian 导入 导入的笔记数(已提交) 逐篇的触发器事件给每篇建索引;任务面板里看得到这些任务
微站构建 已排队 + 构建 id 等待者被 microsite.build.settled(按 owner 分键的 NOTIFY)唤醒,不再靠进程内信号
jobs.fetch_new 所有源 20 秒内抓完就返回职位列表;否则返回 {job_ids, pending: true} jobs.fetch_result {job_ids} 返回同一形状
webhook 对 owner 的写入响应里不承诺任何投递 投递日志

最多等 2 秒,等不到给回执

sequenceDiagram
  participant AI as owner's AI (MCP)
  participant UC as corpus.create
  participant DB as Postgres
  participant Q as Queue
  participant IX as corpus.index
  AI->>UC: create wiki
  UC->>DB: write + event (same transaction) and commit
  UC->>Q: LatestFor → AwaitFanout → Wait, up to 2 s in all
  alt done within 2 s (common)
    Q->>IX: Handle
    IX-->>Q: ok
    Q-->>UC: completed
    UC-->>AI: id + indexed: true
  else timeout (index slow or retrying)
    UC-->>AI: id + indexed: false + index_job_id
    Note over Q,IX: the job keeps running, and retries with backoff on failure
  end

等待分三步:找到这篇笔记最新的 corpus.note.changed 事件(LatestFor);等 relay 把它扇出(AwaitFanout,由 NOTIFY standmeet_events_fanned 唤醒);再等 corpus.index 任务(Wait,由 NOTIFY standmeet_job_final 唤醒)。每一步都靠每个进程一条共享的 LISTEN 连接,不是每个请求各开一个连接轮询。每个频道最多 64 个等待者,超了就直接返回 indexed: false,不排队(concurrency-control)。完结信号是一条 pg_notify,不是 outbox 事件(completion-hooks)。

常见情况下,回执的耗时是 index 队列的一次轮询(≤ 100 毫秒,concurrency-control)加 River 批量完结器的周期:它每 250 毫秒记一次完结。

文案规则

这条规则也管文案:UI 和 MCP 的文字只说已确认的事,“已发送”只在任务 completed 后出现。