// ============================================================================ // memory/search/recall-scheduler.ts — L1 召回会话级调度闸门 // // 存在理由(2026-09-15,plan/memory-system/L1_RECALL_ASYNC/): // 1. **串行**:同一 session 任一时刻至多一个召回活动在飞。并发召回会重复占用 // embedding 配额、引入陈旧结果竞态、并发访问 sqlite。 // 2. **闸门释放绑定「底层真结束」**:不能用 race 结果释放——超时只意味着「这个结果 // 不再值得用」,底层 fetch / QPM 排队可能仍在跑。用 race 释放会让下一个请求与其并发, // 直接违反串行(R1 阻断 B1)。 // 3. **请求语义分层**(R1 阻断 B2): // - inject(自动注入):可丢弃。单槽 latest-wins;被新请求覆盖者以 `superseded` settle。 // - demand(`recall_l1_records` 工具 / blocking 等待):**必须 settle**(结果或错误)。 // 请求-响应语义,promise 永不返回会挂死 agent loop 的工具阶段。 // 4. **兜底**(R1 阻断 B3):底层可能永不 settle(取消不到的网络等待);`maxHoldMs` // 到点强制释放闸门并 warn,避免死锁级故障。 // 5. **优先级**:闸门空闲时 **demand 先于 inject** —— 有调用方在等(agent loop), // 不能让工具阶段排在机会性的自动注入后面。 // // 设计约束:本模块**零业务依赖**(不 import 扩展其他模块);`run`、超时、时钟、定时器 // 全部由调用方注入,便于离线单测(scripts/test-recall-scheduler.mjs)。 // ============================================================================ /** 请求类型:inject 可丢弃;demand 必须 settle。 */ export type RecallKind = "inject" | "demand"; /** 结果落定方式。 */ export type RecallOutcome = "ok" | "timeout" | "superseded" | "error" | "reset"; export interface RecallRequest

{ kind: RecallKind; /** 轮次编号(调用方维护):结果写入前用于判断是否已过期。demand 可传 -1(不参与作废)。 */ turnToken: number; /** 本次「结果可用性」预算(ms):到点即 settle 为 timeout(底层仍在跑,闸门仍被持有)。 */ timeoutMs: number; /** 传给 run 的载荷(查询文本、profile 路径、topK、是否计数等由调用方决定)。 */ payload: P; } export interface RecallSettlement { /** 命中结果;timeout/superseded/error/reset 时为 null。 */ value: T | null; outcome: RecallOutcome; /** error 时的原因文本(便于日志/工具返回)。 */ error?: string; } export interface RecallSchedulerOptions { /** 实际执行体(如 recallL1Records 的封装)。抛错由调度器兜底转 error,不冒泡。 */ run: (payload: P, signal: AbortSignal) => Promise; /** 闸门最长持有时间(ms),超过强制释放 + warn(防底层永不 settle 死锁)。 */ maxHoldMs: number; log?: (msg: string) => void; timers?: { setTimeout: (fn: () => void, ms: number) => unknown; clearTimeout: (handle: unknown) => void; }; } export interface RecallScheduler { /** 提交请求。返回的 promise **必定 settle**(任何 kind、任何路径)。 */ request(req: RecallRequest

): Promise>; /** 是否有活动在飞或排队。 */ isBusy(): boolean; /** 观测计数。 */ stats(): Record & { maxHoldForced: number }; /** 清空状态(profile 切换 / session 关闭):在飞与排队的请求以 `reset` settle,并尽力 abort 底层。 */ reset(): void; } interface Entry { req: RecallRequest

; /** 已 settle 后重复调用为空操作(幂等)。 */ resolve: (s: RecallSettlement) => void; settled: boolean; abort: AbortController; } export function createRecallScheduler(opts: RecallSchedulerOptions): RecallScheduler { const timers = opts.timers ?? { setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: (h) => clearTimeout(h as ReturnType), }; const log = opts.log ?? (() => {}); const counts: Record = { ok: 0, timeout: 0, superseded: 0, error: 0, reset: 0 }; let maxHoldForced = 0; /** 当前在飞者:闸门由它持有,直到底层 settle 或 maxHoldMs 强制释放。 */ let inflight: Entry | null = null; /** inject 单槽(只保留最新;旧的立即 superseded)。 */ let pendingInject: Entry | null = null; /** demand FIFO(每个都会被执行并 settle)。 */ const demandQueue: Entry[] = []; let generation = 0; function settle(entry: Entry, s: RecallSettlement): void { if (entry.settled) return; entry.settled = true; counts[s.outcome]++; entry.resolve(s); } function makeEntry(req: RecallRequest

): { entry: Entry; promise: Promise> } { let raw!: (s: RecallSettlement) => void; const promise = new Promise>((r) => { raw = r; }); const entry: Entry = { req, abort: new AbortController(), settled: false, resolve: (s) => raw(s), }; return { entry, promise }; } /** 启动一个 entry:预算决定「结果是否还值得用」,闸门绑「底层真结束」。 */ function start(entry: Entry): void { const gen = generation; const signal = entry.abort.signal; let underlying: Promise; try { underlying = Promise.resolve(opts.run(entry.req.payload, signal)); } catch (e) { const msg = e instanceof Error ? e.message : String(e); settle(entry, { value: null, outcome: "error", error: msg }); underlying = Promise.resolve(null); } underlying = underlying.catch((e) => { // 主动 abort 引发的失败不算错误(超时/覆盖/重置都会 abort) if (!signal.aborted) { log(`[recall-scheduler] run 抛错: ${e instanceof Error ? e.message : String(e)}`); } return null; }); // ── 结果可用性:预算内完成 → ok;到点 → timeout(底层继续跑,闸门不在此释放) ── const resultTimer = timers.setTimeout(() => { settle(entry, { value: null, outcome: "timeout" }); try { entry.abort.abort(); // 尽力取消底层(HTTP 可取消;QPM sleep 不可取消,靠 maxHoldMs 兜底) } catch { /* ignore */ } }, Math.max(1, entry.req.timeoutMs)); // ── 闸门持有:等底层真结束;maxHoldMs 到点强制释放 ── const holdTimer = timers.setTimeout(() => { if (!inflight || inflight !== entry) return; maxHoldForced++; log(`[recall-scheduler] 闸门强制释放:底层超过 maxHoldMs=${opts.maxHoldMs}ms 未 settle`); release(); }, Math.max(1, opts.maxHoldMs)); inflight = entry; underlying.then((value) => { timers.clearTimeout(resultTimer); timers.clearTimeout(holdTimer); settle(entry, { value, outcome: "ok" }); // value===null → ok 但空结果 if (inflight === entry) { release(); } }); function release(): void { if (gen !== generation) return; // reset 后不再续跑队列(generation 已变) if (inflight === entry) inflight = null; drain(); } } /** 闸门空闲时取下一个:demand 优先(有 agent loop 在等),再取 inject。 */ function drain(): void { if (inflight) return; if (demandQueue.length > 0) { start(demandQueue.shift()!); return; } if (pendingInject) { const e = pendingInject; pendingInject = null; start(e); } } return { request(req) { const { entry, promise } = makeEntry(req); if (req.kind === "demand") { demandQueue.push(entry); } else { if (pendingInject) settle(pendingInject, { value: null, outcome: "superseded" }); pendingInject = entry; } drain(); return promise; }, isBusy: () => inflight !== null || pendingInject !== null || demandQueue.length > 0, stats: () => ({ ...counts, maxHoldForced }), reset() { generation++; try { inflight?.abort.abort(); } catch { /* ignore */ } if (inflight) settle(inflight, { value: null, outcome: "reset" }); inflight = null; if (pendingInject) settle(pendingInject, { value: null, outcome: "reset" }); pendingInject = null; for (const e of demandQueue.splice(0)) settle(e, { value: null, outcome: "reset" }); }, }; }