/** * adopt-route.ts * * 为没有 BOTMUX_* 环境变量的"孤立" Claude 进程(即通过 /adopt 接管的外部 CLI) * 提供 askUserQuestion hook 的路由解析逻辑。 * * 通过以下步骤确定目标 Lark 会话: * 1. 收集 hook 进程的祖先 PID 链 * 2. 遍历在线 daemon,查询每个 daemon 是否有以某祖先 PID 启动的 adopt 会话 * 3. 首个命中即返回路由信息 */ /** 从 daemon 取回的 adopt 会话路由信息 */ export interface AdoptRoute { sessionId: string; chatId: string; larkAppId: string; rootMessageId: string; } /** * 沿进程祖先链向上收集 PID(不含 startPid 自己)。 * * @param startPid 起始进程 PID(自身不包含在结果中) * @param readParent 注入式父 PID 读取函数(默认使用 /proc 或 ps) * @param maxDepth 最大深度,防止意外无限循环(默认 40) * @returns 祖先 PID 数组,从最近父进程到最远祖先 */ export declare function getAncestorPids(startPid: number, readParent?: (pid: number) => number | null, maxDepth?: number): number[]; /** * 查询某个 daemon 是否有以指定 PID 启动的活跃 adopt 会话。 * * GET http://127.0.0.1:/api/adopt-session/ * 200 → 解析 AdoptRoute;其它状态码或异常 → null(不抛)。 * 超时:2 秒(AbortController)。 */ export declare function queryAdoptSession(ipcPort: number, pid: number): Promise; /** * 通过祖先 PID 匹配在线 adopt 会话。 * * **并发 + 全局 budget 封顶**:候选 = daemon 列表序 × 祖先链序(由近及远)逐个编号; * 全部并发查询(每请求各自带 2s 超时),整体不超过 `budgetMs`。命中按候选 index 取 * 最小,保持确定性。 * * 为何要全局 budget:runHook 在缺 BOTMUX_* 时同步 await 本函数,而全局 hook 会覆盖 * 非 botmux 的 Claude 会话;若某 daemon still-online 但 IPC 不响应,顺序 await 会 * `祖先数 × 2s × daemon 数` 线性叠加(可达几十秒),把真·非 botmux 的 ask 卡死。 * 并发让总耗时收敛到单请求量级,budget 再封顶,保证快速 passthrough。 * * @param deps.startPid hook 进程自身的 PID * @param deps.listDaemons 列出在线 daemon(ipcPort) * @param deps.queryDaemon 查询某 daemon 是否有该 pid 的活跃 adopt 会话 * @param deps.getAncestors 取祖先 PID(默认使用 getAncestorPids) * @param deps.budgetMs 整体耗时上限(默认 1500ms;可注入便于测试) */ export declare function resolveAdoptRoute(deps: { startPid: number; listDaemons: () => Array<{ ipcPort: number; }>; queryDaemon: (ipcPort: number, pid: number) => Promise; getAncestors?: (startPid: number) => number[]; budgetMs?: number; }): Promise; /** * 单 daemon 的 cliSessionId 反查结果。 * - hit:该 daemon 内恰好一个活跃会话绑定该 cliSessionId * - conflict:该 daemon 内多个会话绑定同一 cliSessionId(两个话题/机器人并发 * 导入同一外部会话可正常产生重复绑定,cliSessionId 不代表 botmux 绑定唯一) * - miss:该 daemon 明确无匹配 * - unknown:查询失败/超时,结果未知 → 上层必须视为「无法证明唯一」fail closed */ export type CliSessionLookup = { kind: 'hit'; route: AdoptRoute; } | { kind: 'conflict'; } | { kind: 'miss'; } | { kind: 'unknown'; }; /** * 查询某个 daemon 是否有该 CLI 原生会话 id(如 OpenCode 的 `ses_*`)的活跃 * 会话。反查 identity 是 (cliId, cliSessionId):OpenCode V1→V2 迁移会原样保留 * ses_* id,必须带上发起反查的适配器 cliId,否则 V1 会话会被误当成唯一 hit。 * 本端点只服务 opencode2 共享托管 service 的反查(服务端强制 cliId===opencode2)。 * GET http://127.0.0.1:/api/session-by-cli/opencode2/ * 200 → hit;409(重复绑定)→ conflict;404 且 body 为本 endpoint 专用响应 * `{error:'no_session'}` → miss(明确无匹配);generic 404 / 401 / 403 / 5xx * 等其它状态与无效 body → unknown(协议/查询结果未知,上层必须视为「无法证明 * 唯一」fail closed)。超时 2s。 */ export declare function queryCliSession(ipcPort: number, cliSessionId: string): Promise; /** * 并发反查全部在线 daemon,按 cliSessionId 找所属 botmux 会话。 * * 托管 service 场景(opencode2 的 ask 插件运行在所有客户端共用的 service 里): * hook 子进程继承的是「启动该 service 的会话」的 ambient env,与当前会话无关, * 不能拿来路由 —— 必须用 payload 携带的 native sessionID 显式反查,否则跨会话 * 错投。 * * **fail closed(恰好一个完整命中才返回)**:必须证明命中唯一才返回路由, * 以下任一情况都返回 null(由调用方 passthrough),绝不按网络时序挑一个命中: * - 0 个命中 → 未命中 * - ≥2 个命中(并发导入同一外部会话的重复绑定)→ 歧义 * - 任一 daemon 报 conflict → 歧义 * - 任一候选在 budget 内未确定结果(挂起/异常/unknown)→ 无法证明唯一 * * @param deps.cliSessionId OpenCode 原生会话 id(payload.session_id) * @param deps.listDaemons 列出在线 daemon(ipcPort) * @param deps.queryDaemon 查询某 daemon 是否有该 cliSessionId 的活跃会话 * @param deps.budgetMs 整体耗时上限(默认 1500ms;可注入便于测试) */ export declare function resolveCliSessionRoute(deps: { cliSessionId: string; listDaemons: () => Array<{ ipcPort: number; }>; queryDaemon: (ipcPort: number, cliSessionId: string) => Promise; budgetMs?: number; }): Promise; //# sourceMappingURL=adopt-route.d.ts.map