@x-otto/runtime 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["MAX_TITLE_LENGTH","CAP_LOW_WATERMARK_FRACTION","decideResidency","decideResidency","DEFAULT_SAMPLE_INTERVAL_MS","DEFAULT_MAX_PROCESSES","DEFAULT_MAX_PROCESSES","reaperInstalled","nodeSpawn","TERMINAL"],"sources":["../src/engine-stores.ts","../src/session-stable.ts","../src/context-source.ts","../src/context-sources/section-order.ts","../src/context-sources/environment.ts","../src/context-sources/memory-index.ts","../src/session/hooks.ts","../src/session/derive-title.ts","../src/session/generate-title.ts","../src/session/session-title-manager.ts","../src/session/session-persistence-bridge.ts","../src/session/todo-gate-runner.ts","../src/session/agent-session-compaction.ts","../src/session/agent-session-event-forwarding.ts","../src/session/tool-result-store.ts","../src/session/tool-result-store-global.ts","../src/defaults.ts","../src/session/pending-gate.ts","../src/session/agent-session-gates.ts","../src/session/agent-session.ts","../src/session/fork.ts","../src/session/checkpoint.ts","../src/session/trace-decode.ts","../src/session/traceback.ts","../src/session/replay.ts","../src/session/sandbox.ts","../src/session/time-travel-controller.ts","../src/session/residency-governor.ts","../src/session/crash-gap-detector.ts","../src/session/global-residency-engine.ts","../src/session/residency-executor.ts","../src/session/residency-action-coordinator.ts","../src/session/panel-state-restorer.ts","../src/session/panel-state-store.ts","../src/session/persistence-sync.ts","../src/session/session-manager.ts","../src/create-agent-runtime.ts","../src/memory-governor.ts","../src/fleet-monitor.ts","../src/process-kill-primitives.ts","../src/process-registry.ts","../src/process-runtime.ts","../src/git-worktree.ts","../src/governance/forbidden-zone.ts","../src/governance/capability-surfaces.ts","../src/governance/apply-policy.ts","../src/agent-job-registry.ts","../src/agent-observability.ts","../src/context-sources/memory-delta.ts","../src/context-sources/todo-reminder.ts","../src/context-sources/doc-sync-reminder.ts","../src/session/residency-allocator.ts"],"sourcesContent":["import { join } from 'node:path'\nimport {\n FileAppendLog,\n HttpAppendLog,\n MemoryAppendLog,\n type AppendLog,\n} from '@x-otto/persistence'\nimport type { TraceEvent } from '@x-otto/agent'\nimport type { CheckpointData } from './session/checkpoint'\n\n/**\n * trace/checkpoint AppendLog 后端选择的**单一权威**。\n *\n * 坑:remote 后端必须用 Http 变体,否则远端会话的 trace 落内存、重启即丢。\n */\nexport interface EngineStoreLocation {\n /** 会话快照落盘目录(disk 后端):trace/checkpoint 落 <dir>/trace、<dir>/checkpoint。最高优先。 */\n sessionDir?: string\n /** 远端 AppendLog 选项(remote 后端,HttpAppendLog);sessionDir 在场时被忽略。\n * fetch 可选——HttpAppendLog 内部对缺省回落 globalThis.fetch(兼容 App 的可选 remote.fetch)。 */\n remoteLog?: {\n baseUrl: string\n getAuth: () => Promise<{ token: string }>\n fetch?: typeof globalThis.fetch\n timeoutMs?: number\n }\n /**\n * RFC-345 §D8:影子模式旗标。为真时**强制** trace/checkpoint 走 MemoryAppendLog,\n * 即便 sessionDir/remoteLog 在场。\n *\n * 为何需要独立旗标而非\"不传 sessionDir\":sessionDir 对其他子系统(如 settings、\n * lesson 等非会话存储的目录惯例)仍可能有用途,不能靠\"不给目录\"顺带关掉 trace 落盘。\n * ephemeral 让 devtools.inspect() 的实时观测(LiveTap 内存旁路)在影子态仍可用,\n * 而磁盘零写入。由 §D1 白名单装配点(storage-wiring / create-agent-runtime)注入。\n */\n ephemeral?: boolean\n}\n\nexport interface EngineStoreOverrides {\n /** 显式注入的 trace store(最高优先,覆盖一切后端选择)。 */\n traceStore?: AppendLog<TraceEvent>\n /** 显式注入的 checkpoint store(最高优先)。 */\n checkpointStore?: AppendLog<CheckpointData>\n}\n\n/**\n * 选择 trace/checkpoint 后端。优先级:显式 override > ephemeral(Memory) >\n * sessionDir(File) > remoteLog(Http) > Memory。\n */\nexport function buildEngineStores(\n loc: EngineStoreLocation,\n overrides?: EngineStoreOverrides,\n): { traceStore: AppendLog<TraceEvent>; checkpointStore: AppendLog<CheckpointData> } {\n const make = <E>(sub: string): AppendLog<E> =>\n loc.ephemeral\n ? new MemoryAppendLog<E>()\n : loc.sessionDir\n ? new FileAppendLog<E>({ baseDir: join(loc.sessionDir, sub) })\n : loc.remoteLog\n ? new HttpAppendLog<E>(loc.remoteLog)\n : new MemoryAppendLog<E>()\n return {\n traceStore: overrides?.traceStore ?? make<TraceEvent>('trace'),\n checkpointStore: overrides?.checkpointStore ?? make<CheckpointData>('checkpoint'),\n }\n}\n","/**\n * session-stable.ts(M14-02)\n *\n * 注入稳定化基座:system prompt 注入块按 sessionId **冻结**(首轮计算,会话内复用),\n * `session.deleted` 驱逐。这是 prompt cache(M14-01 断点)真正命中的前提——\n * 此前 environment(含时间戳/git 状态)等注入每轮重算,system 前缀逐轮漂移,\n * 断点形同虚设。原则:system prompt 会话内稳定,动态信息走消息流。\n */\n\nimport { defineHook } from '@x-otto/hooks'\n\nimport type { HookSpec, SystemPromptTransformInput } from '@x-otto/hooks'\n\nexport interface SessionStableInjectionOptions {\n name: string\n priority: number\n /** 首轮计算注入块;返回空/undefined 表示本会话无注入(也会被冻结,不再重算)。 */\n compute: (input: SystemPromptTransformInput) => Promise<string | undefined> | string | undefined\n}\n\nexport function createSessionStableInjectionHooks(\n options: SessionStableInjectionOptions,\n): HookSpec[] {\n const cache = new Map<string, string>()\n\n const inject = defineHook({\n name: options.name,\n timing: 'system.prompt.transform',\n priority: options.priority,\n handle: async (input, output) => {\n let block = cache.get(input.sessionId)\n if (block === undefined) {\n block = (await options.compute(input)) ?? ''\n cache.set(input.sessionId, block)\n }\n if (block) {\n output.systemPrompt = `${output.systemPrompt}\\n\\n${block}`\n }\n },\n })\n\n const cleanup = defineHook({\n name: `${options.name}-cleanup`,\n timing: 'session.deleted',\n priority: 90,\n handle: (input) => {\n cache.delete((input as { sessionId: string }).sessionId)\n },\n })\n\n return [inject, cleanup]\n}\n","import { createSessionStableInjectionHooks } from './session-stable'\n\nimport type { HookSpec, SystemPromptTransformInput } from '@x-otto/hooks'\n\nexport interface ContextSource {\n /** 本源的唯一标识。重复注册→覆盖前一次同 id 源。 */\n readonly id: string\n /** 排序权重(越小越靠前,与 HookSpec.priority 同语义)。缺省 50。 */\n readonly priority?: number\n /**\n * 计算本轮注入的上下文片段。由 session-stable 底座做会话内冻结;\n * 返回空/undefined 视为此轮无贡献。\n */\n contribute(ctx: ContextBuildInput): Promise<string | undefined> | string | undefined\n}\n\n/**\n * RFC-067 M113(A3/D3):memory 的**上下文注入**语义端口(三端口之一)。此前是 ContextBuildInput.memory\n * 上的内联匿名类型,消费方靠 `typeof loadMemory==='function'` 鸭子探测还原——现显式命名,供 typed 装配。\n */\nexport interface ContextInjectionPort {\n loadMemory(): Promise<void>\n getMemoryPrompt(): string\n}\n\nexport interface ContextBuildInput {\n readonly sessionId: string\n /** 工作区目录(用于读取文件/检测环境)。 */\n readonly workspaceDir?: string\n /** 可选的 memory 上下文注入端口。 */\n readonly memory?: ContextInjectionPort\n}\n\nexport class ContextSourceRegistry {\n private readonly sources = new Map<string, ContextSource>()\n private sealed = false\n\n register(source: ContextSource): void {\n if (this.sealed) {\n throw new Error(\n `ContextSourceRegistry: cannot register \"${source.id}\" — registry is sealed (hooks already built).`,\n )\n }\n this.sources.set(source.id, source)\n }\n\n list(): ReadonlyArray<ContextSource> {\n return [...this.sources.values()].sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))\n }\n\n /**\n * 产出 HookSpec[](经 session-stable 包装),可注册进 hookRegistry。\n * 调用后 registry 只读——不允许运行时增删源。\n *\n * `workspaceDir` 和 `memory` 在 hook 构建期闭包捕获(由 createAgentRuntime 传入);\n * `sessionId` 由 hook 运行时 SystemPromptTransformInput 提供(每个会话独立)。\n *\n * RFC-129 D4:重命名自 `buildHooks()`(纯改名,行为不变——调用即 seal)。命名变化表达\n * 调用方必须保证的不变量:插件贡献的 context source(若有)必须在调用本方法**之前**\n * 完成 `register()`(App 侧调用顺序见 RFC-129 D4 精确插入位置);本方法自身不感知\n * 插件、不做任何\"等待插件\"的逻辑——它仍是\"此刻立即 seal 并构建\"的同步语义。\n */\n sealAndBuildHooks(workspaceDir?: string, memory?: ContextBuildInput['memory']): HookSpec[] {\n this.sealed = true\n const hooks: HookSpec[] = []\n\n for (const source of this.list()) {\n hooks.push(\n ...createSessionStableInjectionHooks({\n name: `ctxsrc:${source.id}`,\n priority: source.priority ?? 50,\n compute: async (input: SystemPromptTransformInput) =>\n source.contribute({ sessionId: input.sessionId, workspaceDir, memory }),\n }),\n )\n }\n\n return hooks\n }\n}\n","/**\n * RFC-020 Phase 3(M54)P3-04:system-prompt 注入 section 的**唯一有序声明**。\n *\n * 治病灶——此前各 section 的 priority 数字(20/25/35/38/40/60)散在 coding hooks 与 runtime\n * context-sources 两包里,\"system prompt 各段什么顺序\"得捞齐多处数字才能拼出。现收敛为此处一张表:\n * - 数组/字段顺序即 priority 升序 = 实际 `system.prompt.transform` hook 的执行/拼接顺序;\n * - `lane` 标明该段落 **stable 前缀**(systemPrompt,进 prompt cache)还是 **volatile 尾段**\n * (systemTail,落在 cache 断点之后,逐轮可变不击穿缓存);\n * - runtime context-sources(environment/memory-index/memory-delta)与 coding hooks\n * (tool-guidance/lesson/skill-catalog)均从此处取 priority —— 单一真相源,改序只此一处。\n *\n * 放在 @x-otto/runtime(coding 依赖 runtime、反向不依赖):coding 侧 hook 经 `@x-otto/runtime` import。\n * 注:本表是**排序权威**(方案 H:section 仍由各自 hook 自注册,priority 数字降级为对本表的引用);\n * 不强行把 `SYSTEM_PROMPT_SECTIONS` 各 section 物理并入单一 registry(那超出外科尺度、非目标)——\n * section 数量以该常量的字段数为准,不在本段注释里重复写死具体数字(history: 建表时 7 个,\n * 后追加 docSyncReminder 成 8 个,写死数字会重犯本次发现的漂移)。\n */\n\nexport type SystemPromptLane = 'stable' | 'volatile'\n\nexport interface SystemPromptSectionSpec {\n /** 对应 hook / ContextSource 的 name/id(与注册名一致,供 drift guard 比对)。 */\n readonly name: string\n readonly priority: number\n readonly lane: SystemPromptLane\n}\n\nexport const SYSTEM_PROMPT_SECTIONS = {\n toolGuidance: { name: 'tool-guidance-injection', priority: 20, lane: 'stable' },\n /**\n * RFC-318 D7:三分流判别(缺工具 / 流程重复 / 其余)。紧跟 toolGuidance——它是对\n * \"现有工具够不够用\"的元判断,语义上属工具指引的延伸;内容恒定故走 stable lane。\n */\n skillLoopGuidance: { name: 'skill-loop-guidance-injection', priority: 21, lane: 'stable' },\n environment: { name: 'runtime:environment', priority: 25, lane: 'stable' },\n memoryIndex: { name: 'runtime:memory-index', priority: 35, lane: 'stable' },\n skillCatalog: { name: 'skill-catalog-injection', priority: 38, lane: 'stable' },\n lesson: { name: 'lesson-injection', priority: 40, lane: 'stable' },\n memoryDelta: { name: 'memory-delta-injection', priority: 60, lane: 'volatile' },\n todoReminder: { name: 'todo-reminder-injection', priority: 62, lane: 'volatile' },\n docSyncReminder: { name: 'doc-sync-reminder-injection', priority: 64, lane: 'volatile' },\n} as const satisfies Record<string, SystemPromptSectionSpec>\n\n/** 按 priority 升序的 section 序列(= 实际组装顺序的显式视图)。 */\nexport const SYSTEM_PROMPT_SECTION_ORDER: readonly SystemPromptSectionSpec[] =\n Object.values(SYSTEM_PROMPT_SECTIONS)\n","import { collectEnvironment, formatEnvironmentBlock } from '@x-otto/prompt'\n\nimport { SYSTEM_PROMPT_SECTIONS } from './section-order'\nimport type { ContextSource, ContextBuildInput } from '../context-source'\n\nexport function createEnvironmentContextSource(workspaceDir: string): ContextSource {\n return {\n id: 'runtime:environment',\n priority: SYSTEM_PROMPT_SECTIONS.environment.priority,\n async contribute(_ctx: ContextBuildInput) {\n // RFC-189 R3 已知例外:每回合构建系统提示词时的环境信息收集,execSync 超时 5000ms。\n // 可异步化为 exec(本函数已是 async),但改造涉及 @x-otto/prompt 的 collectEnvironment\n // 调用签名联动,超出 RFC-189 范围(该 RFC 只治理 !/!! 主命令路径),记为独立观察项。\n const exec = async (cmd: string, cwd: string) => {\n const { execSync } = await import('node:child_process')\n return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000 })\n }\n try {\n const env = await collectEnvironment(workspaceDir, exec)\n return formatEnvironmentBlock(env)\n } catch {\n return undefined\n }\n },\n }\n}\n","import { SYSTEM_PROMPT_SECTIONS } from './section-order'\nimport type { ContextSource, ContextBuildInput } from '../context-source'\n\nexport function createMemoryIndexContextSource(\n _workspaceDir: string,\n memory?: ContextBuildInput['memory'],\n): ContextSource {\n let loaded = false\n\n return {\n id: 'runtime:memory-index',\n priority: SYSTEM_PROMPT_SECTIONS.memoryIndex.priority,\n async contribute(_ctx: ContextBuildInput) {\n if (!memory) {\n return undefined\n }\n if (!loaded) {\n await memory.loadMemory()\n loaded = true\n }\n const injection = memory.getMemoryPrompt()\n return injection.trim() ? injection : undefined\n },\n }\n}\n","import type { ToolBeforeOutcome, ToolHookExecutor, ToolResult } from '@x-otto/agent'\nimport type { ToolExecuteBeforeOutput } from '@x-otto/hooks'\nimport type { HookRegistry } from '@x-otto/hooks'\nimport { createLogger } from '@x-otto/shared'\n\nconst logger = createLogger('@x-otto/runtime/hooks')\n\ninterface ToolHookOptions {\n hookRegistry: HookRegistry\n agentName: string\n sessionId: string\n}\n\nexport function createToolHookExecutor(options: ToolHookOptions): ToolHookExecutor {\n const { hookRegistry, agentName, sessionId } = options\n\n return {\n async executeBeforeHooks(input): Promise<ToolBeforeOutcome> {\n const output: ToolExecuteBeforeOutput = {\n args: { ...input.args },\n cancelled: false,\n }\n\n await hookRegistry.execute(\n 'tool.execute.before',\n {\n ...input,\n agentName,\n sessionId,\n },\n output,\n )\n\n if (output.cancelled || output.decision === 'deny') {\n return {\n kind: 'deny',\n reason:\n output.cancelReason ??\n output.decisionReason ??\n `Tool ${input.toolName} was blocked by pre-execution hook`,\n }\n }\n\n if (output.decision === 'ask') {\n return {\n kind: 'ask',\n args: output.args,\n reason: output.decisionReason ?? 'Requires confirmation',\n risk: output.risk,\n }\n }\n\n if (output.decision === 'allow' || output.decision === undefined) {\n return { kind: 'proceed', args: output.args }\n }\n\n logger.warn(\n { toolName: input.toolName, decision: output.decision },\n 'tool.execute.before hook returned unrecognized decision — denying as safety fallback',\n )\n return {\n kind: 'deny',\n reason: `Tool ${input.toolName} was blocked: hook returned unrecognized decision \"${String(output.decision)}\"`,\n }\n },\n\n async executeAfterHooks(input) {\n const output: { result: ToolResult; metadata: Record<string, unknown> } = {\n result: input.result,\n metadata: {},\n }\n\n await hookRegistry.execute(\n 'tool.execute.after',\n {\n ...input,\n agentName,\n sessionId,\n },\n output,\n )\n\n return output\n },\n }\n}\n","import type { AgentMessage } from '@x-otto/agent'\n\nconst MAX_TITLE_LENGTH = 80\n\n/**\n * 从会话消息列表中派生标题(RFC-030 P1)。\n *\n * 回落链:meta.title → deriveSessionTitle(messages()) → id.slice(0, 8)\n *\n * 策略:\n * - 找第一条 role==='user' 的真实消息(跳过 system/assistant/summary 标记消息)。\n * - 跳过 compact_boundary 子类型的系统消息(summary 边界标记,不是用户输入)。\n * - 文本归一:换行/多空格 → 单空格,trim,截 MAX_TITLE_LENGTH 字符。\n * - 空则返回 undefined,由调用方继续回落到 id.slice(0, 8)。\n */\nexport function deriveSessionTitle(messages: readonly AgentMessage[]): string | undefined {\n for (const msg of messages) {\n if (msg.role !== 'user') continue\n\n const { content } = msg as { role: string; content: unknown }\n let text: string | undefined\n\n if (typeof content === 'string') {\n text = content\n } else if (Array.isArray(content)) {\n const parts = content as Array<{ type?: string; text?: string }>\n text = parts\n .filter((p) => p.type === 'text' && typeof p.text === 'string')\n .map((p) => p.text as string)\n .join(' ')\n }\n\n if (!text) continue\n\n const normalized = text.replace(/\\s+/g, ' ').trim()\n if (!normalized) continue\n\n return normalized.length > MAX_TITLE_LENGTH\n ? normalized.slice(0, MAX_TITLE_LENGTH)\n : normalized\n }\n\n return undefined\n}\n\n/**\n * 统一标题回落链(RFC-078 D1/R5 单一真源)——三表面(service / 终端 / resume)共用。\n *\n * 回落:persistedTitle(meta.title,已是最高优先来源) → deriveSessionTitle(首条 user 消息) → id 前 8 位。\n *\n * 说明:`titleSource` 只在**写入**时决定覆盖(见 InMemorySession.setTitle 等级守卫),\n * 故读取时 `meta.title` 必为当前最高优先值,本函数无需再分辨来源。\n */\nexport function resolveSessionTitle(\n persistedTitle: string | undefined,\n messages: readonly AgentMessage[],\n id: string,\n): string {\n const trimmed = persistedTitle?.trim()\n if (trimmed) return trimmed\n return deriveSessionTitle(messages) ?? id.slice(0, 8)\n}\n","import type { Message, Model, StreamFunction } from '@x-otto/ai'\n\n/**\n * LLM 标题生成(RFC-078 M134 / D2/D3)。\n *\n * 设计要点:\n * - 复用会话已注入的 `StreamFunction`(按 context.model.api 路由 provider),故无需自带 provider registry;\n * 只需传入一个**便宜模型**(由 app 层经 RFC-061 `category:'search'` + usability gate 解析)。\n * - 一次性结构化补全:system 给 sentence-case 3-7 词指令 + 要求 JSON `{title}`;fail-soft → null。\n * - 超时 + abort 串联(R4 绝不阻塞回合:调用方 fire-and-forget;本函数自带超时上限)。\n *\n * prompt 采用 sentence-case 版(见 RFC-078 §4)。\n */\n\nconst SESSION_TITLE_PROMPT = `Generate a concise, sentence-case title (3-7 words) that captures the main topic or goal of this coding session. The title should be clear enough that the user recognizes the session in a list. Use sentence case: capitalize only the first word and proper nouns.\n\nReturn JSON with a single \"title\" field.\n\nGood examples:\n{\"title\": \"Fix login button on mobile\"}\n{\"title\": \"Add OAuth authentication\"}\n{\"title\": \"Debug failing CI tests\"}\n\nBad (too vague): {\"title\": \"Code changes\"}\nBad (too long): {\"title\": \"Investigate and fix the issue where the login button does not respond on mobile devices\"}\nBad (wrong case): {\"title\": \"Fix Login Button On Mobile\"}`\n\nconst DEFAULT_TIMEOUT_MS = 15_000\nconst MAX_INPUT_CHARS = 2000\nconst MAX_TITLE_LENGTH = 80\n\nexport interface TitleGenDeps {\n /** 会话注入的流函数(与对话同一条,按 model.api 路由 provider)。 */\n stream: StreamFunction\n /** 便宜标题模型(RFC-061 解析;禁用对话模型 / claude-OAuth,见 RFC-078 R3,由调用方保证)。 */\n model: Model\n /** 上限超时(ms),默认 15s。 */\n timeoutMs?: number\n}\n\n/** 从 assistant 消息内容里抽出纯文本。 */\nfunction extractText(content: unknown): string {\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content\n .filter((p): p is { type: string; text: string } => {\n const t = p as { type?: string; text?: unknown }\n return t.type === 'text' && typeof t.text === 'string'\n })\n .map((p) => p.text)\n .join('')\n }\n return ''\n}\n\n/**\n * 解析 `{\"title\": \"...\"}`;容忍模型在 JSON 外包裹散文/代码围栏。\n *\n * bug 修复(真实用户反馈:`/status` 标题显示为原始 `{\"title` 片段):`maxTokens:64`\n * 较紧凑,模型偶发在闭合引号/大括号前被截断(或模型自身输出畸形 JSON)时,此前的\n * 正则匹配失败后会直接把**破损的原始 JSON 片段**当作候选标题(`candidate = match?.[1]\n * ?? text` 的 `?? text` 分支)——\"非 JSON 散文\"与\"想输出 JSON 但被截断\"被错误地\n * 合并处理,导致用户看到未解析的 `{\"title...` 碎片而非人类可读标题。\n *\n * 修复:先判断文本是否\"看起来在尝试输出 JSON\"(trim 后以 `{` 开头)。若是,只信任\n * 正则成功抽取的结果;正则失败视为截断/畸形,返回 `null`(fail-soft,调用方回落到\n * 既有 derived 标题,不落地任何破损片段)。若文本压根不以 `{` 开头(真正的纯散文\n * 回复,如模型忽略指令直接说了一句话),保留原有\"整段当标题\"的兼容行为。\n */\nfunction parseTitle(raw: string): string | null {\n const text = raw.trim()\n if (!text) return null\n // 优先抓第一个含 title 字段的 JSON 对象\n const match = text.match(/\\{[^{}]*\"title\"\\s*:\\s*\"([^\"]+)\"[^{}]*\\}/)\n // `\"title` 子串(含引号)是\"模型在尝试输出 JSON\"的强特征——正常散文回复几乎不会\n // 恰好包含这个字面片段。若该特征存在但完整正则未命中,判定为 maxTokens 截断/模型\n // 输出畸形 JSON,不回落到原始破损片段(那不是可读标题,如 `{\"title` 本身)。\n if (!match && /\"title/.test(text)) return null\n const candidate = match?.[1] ?? text\n const normalized = candidate.replace(/\\s+/g, ' ').trim()\n if (!normalized) return null\n return normalized.length > MAX_TITLE_LENGTH ? normalized.slice(0, MAX_TITLE_LENGTH) : normalized\n}\n\n/**\n * 生成会话标题。成功返回归一后的标题;任何失败(超时/网络/解析/中止)返回 null(fail-soft,\n * 调用方保留既有 derived 占位)。\n */\nexport async function generateTitle(\n deps: TitleGenDeps,\n conversationText: string,\n signal: AbortSignal,\n): Promise<string | null> {\n const text = conversationText.trim().slice(-MAX_INPUT_CHARS)\n if (!text) return null\n\n const controller = new AbortController()\n const onAbort = (): void => controller.abort()\n if (signal.aborted) return null\n signal.addEventListener('abort', onAbort, { once: true })\n const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n\n try {\n const messages: Message[] = [\n { role: 'user', timestamp: Date.now(), content: [{ type: 'text', text }] },\n ]\n const result = await deps.stream(\n {\n model: deps.model,\n systemPrompt: SESSION_TITLE_PROMPT,\n messages,\n cacheRetention: 'none',\n maxTokens: 64,\n temperature: 0,\n },\n controller.signal,\n ).result()\n return parseTitle(extractText(result.content))\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n signal.removeEventListener('abort', onAbort)\n }\n}\n","import { generateTitle } from './generate-title'\n\nimport type { Session } from '@x-otto/session'\nimport type { AgentMessage } from '@x-otto/agent'\nimport type { Model, StreamFunction } from '@x-otto/ai'\n\n/**\n * 会话标题生成管理器(post-RFC-092 review F7 从 AgentSession 拆出的独立 concern)。\n *\n * 职责:LLM fire-and-forget 标题生成的守卫链(无 titleModel 跳过 / custom 标题不动 /\n * 仅前 3 用户回合内生成 / 乱序响应丢弃 / dispose 时中止)。\n * 不负责:会话生命周期、消息持久化——只读 session 的 metadata/messages,写 session.setTitle。\n *\n * RFC-078 M134:标题 LLM 生成依赖(便宜模型 + 复用会话 stream)。titleModel 缺省=不生成。\n */\nexport interface SessionTitleManagerOptions {\n session: Session<AgentMessage>\n /** 会话注入的流函数(与对话同一条,按 model.api 路由 provider)。 */\n stream: StreamFunction\n /** 便宜标题模型;缺省=不生成。 */\n titleModel?: Model\n}\n\nexport class SessionTitleManager {\n private readonly session: Session<AgentMessage>\n private readonly stream: StreamFunction\n private readonly titleModel?: Model\n\n /** 进行中标题生成的序号 + 中止器——丢弃乱序响应(genSeq 守卫)、dispose 时中止。 */\n private genSeq = 0\n private readonly abortController = new AbortController()\n private disposed = false\n\n constructor(options: SessionTitleManagerOptions) {\n this.session = options.session\n this.stream = options.stream\n this.titleModel = options.titleModel\n }\n\n /**\n * RFC-078 M134-03:fire-and-forget 生成 ai 标题。\n * 守卫:无 titleModel → 跳过;custom 标题 → 不动;仅前 3 个用户回合内生成(成本上限 + 随对话\n * 变长标题更准,之后冻结)。genSeq 守卫丢弃乱序/被更晚生成取代的响应;setTitle 等级守卫再兜一层。\n */\n maybeGenerate(force = false): void {\n if (!this.titleModel || this.disposed) return\n if (this.session.metadata().titleSource === 'custom') return\n const userTurns = this.session.messages().filter((m) => m.role === 'user').length\n if (userTurns === 0) return\n if (!force && userTurns > 3) return\n\n const seq = ++this.genSeq\n const convText = this.session\n .messages()\n .map((m) => {\n const c = (m as { content?: unknown }).content\n if (typeof c === 'string') return c\n if (Array.isArray(c)) {\n return c\n .filter((p): p is { text: string } => {\n const t = p as { type?: string; text?: unknown }\n return t.type === 'text' && typeof t.text === 'string'\n })\n .map((p) => p.text)\n .join(' ')\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n\n void generateTitle(\n { stream: this.stream, model: this.titleModel },\n convText,\n this.abortController.signal,\n )\n .then((title) => {\n if (!title || this.disposed) return\n if (seq !== this.genSeq) return // 被更晚的生成取代(乱序守卫)\n if (this.session.metadata().titleSource === 'custom') return // 生成期间被改名\n this.session.setTitle(title, 'ai')\n })\n .catch(() => {\n /* fail-soft:generateTitle 已吞错,这里兜底保留 derived 占位 */\n })\n }\n\n /**\n * RFC-078 M135-01:显式回血——对 title 空/derived 的(已加载)会话强制重生 ai 标题,\n * 绕过「前 3 回合」成本闸(回血是一次性批处理意图)。custom 仍受守卫保护。\n */\n backfill(): void {\n this.maybeGenerate(true)\n }\n\n /** 中止任何 in-flight 标题生成(R4),后续调用变 no-op。 */\n dispose(): void {\n if (this.disposed) return\n this.disposed = true\n this.abortController.abort()\n }\n}\n","/**\n * session-persistence-bridge.ts — AgentSession 的 PersistencePort 实现(RFC-118 S3)。\n *\n * 背景(review 2026-07-06 F2/S3):`PersistencePort`(@x-otto/agent 定义的契约)此前以\n * ~50 行内联闭包对象的形式挂在 AgentSession 构造函数里——大部分方法是一层转发\n * (`this.session.xxx()`),但 `recordCompaction`/`notifyPrune` 额外内联了 publish\n * 事件逻辑,使这些副作用行为与 AgentSession 类体耦合、难以独立测试。\n *\n * 提取为独立类:接收 `{ session, publish }` 依赖,实现同一 `PersistencePort` 契约。\n * publish 行为显式化(构造签名即声明依赖),可脱离 AgentSession 单独测试。\n * 依赖方向不变——agent 定义契约,runtime(本类)实现,AgentSession 只做一行\n * `new SessionPersistenceBridge({ session: this.session, publish: (e) => this.publish(e) })`。\n */\nimport type { PersistencePort, AgentMessage, AgentSessionEventMap } from '@x-otto/agent'\nimport type { Session } from '@x-otto/session'\n\nexport interface SessionPersistenceBridgeDeps {\n session: Session<AgentMessage>\n sessionId: string\n publish: (event: Partial<{ [K in keyof AgentSessionEventMap]: Parameters<AgentSessionEventMap[K]> }>) => void\n /**\n * RFC-336 D2:输入边界落盘请求(可选注入)。由 AgentSession 透传至 SessionPool 的\n * `PersistenceSync.requestInputBoundaryFlush`——后者复用既有 per-session `enqueue`\n * 串行链(不新开并发写路径,规则 2/9),fire-and-forget(规则 3)。\n * 未注入时 `persistInputBoundary()` 为 no-op,退回改动前行为(首个 turn 后才落库)。\n */\n requestInputBoundaryFlush?: () => void\n}\n\nexport class SessionPersistenceBridge implements PersistencePort {\n constructor(private readonly deps: SessionPersistenceBridgeDeps) {}\n\n messages(): readonly AgentMessage[] {\n return this.deps.session.messages()\n }\n\n append(message: AgentMessage): void {\n this.deps.session.append(message)\n }\n\n removeLast(): void {\n this.deps.session.removeLast()\n }\n\n /**\n * RFC-336 D2:prep 成功后的输入边界落盘请求(见 `PersistencePort.persistInputBoundary`\n * 的完整契约)。纯转发——落盘策略、串行化与失败降级全在 `PersistenceSync` 侧,\n * 本桥不做判断,保持\"agent 定义契约、runtime 实现\"的既有依赖方向。\n */\n persistInputBoundary(): void {\n this.deps.requestInputBoundaryFlush?.()\n }\n\n buildContext(): { messages: AgentMessage[]; summary?: string } {\n return this.deps.session.buildContext()\n }\n\n persistNew(messages: AgentMessage[], fromIndex: number): void {\n const newMessages = messages.slice(fromIndex)\n for (const msg of newMessages) {\n this.deps.session.append(msg)\n }\n }\n\n recordCompaction(summary: string, replacement: AgentMessage[]): void {\n const { session, sessionId, publish } = this.deps\n const before = session.buildContext().messages.length\n session.recordCompaction(summary, replacement)\n const after = session.buildContext().messages.length\n if (after < before) {\n publish({\n 'compaction.start': [{ type: 'compaction.start' as const, sessionId, messageCount: before }],\n })\n // `summary` 随事件透传(/compact log 详情面板消费)——摘要本身已落 session tree\n // (recordCompaction 写回),此处只是把同一份数据也推给 TUI 侧的 ring-buffer 日志。\n publish({\n 'compaction.end': [\n { type: 'compaction.end' as const, sessionId, retainedCount: after, summary },\n ],\n })\n }\n }\n\n notifyPrune(\n tokensSaved: number,\n metrics?: { prunedCount: number; messagesBefore: number; messagesAfter: number },\n ): void {\n this.deps.publish({\n 'memory.pruned': [\n {\n type: 'memory.pruned' as const,\n sessionId: this.deps.sessionId,\n tokensSaved,\n prunedCount: metrics?.prunedCount,\n messagesBefore: metrics?.messagesBefore,\n messagesAfter: metrics?.messagesAfter,\n },\n ],\n })\n }\n}\n","import type { Logger } from '@x-otto/shared'\nimport type { Session } from '@x-otto/session'\nimport type { AgentMessage } from '@x-otto/agent'\nimport type { AgentSessionEventMap } from './types'\n\n/**\n * RFC-094 续跑触发消息(最小 well-formed user 消息,internal 标记不渲染)。\n * 详细指令由 todo-reminder systemTail 携带(RFC-042 D1 耦合闭环),此处只做触发器。\n */\nexport const TODO_GATE_CONTINUE_PROMPT = 'Continue with the remaining todo items.'\n\n/**\n * RFC-094 增补(无进展判定细化):续跑轮有真实工具活动但 todo 状态零变化时的**定向**触发消息。\n * 场景:模型干了活(甚至活已干完)但没更新 todo 状态——通用续跑指令(\"继续做剩余事项\")在此\n * 场景是误导(可能让模型重做已完成的工作);定向指令直接点名\"先对账 todo 状态\"。\n * 每轮闸门循环最多发一次(syncNudge 单发),防\"干活但永不同步\"的模型进入定向指令无限循环。\n */\nexport const TODO_GATE_SYNC_PROMPT =\n 'You made progress but did not update the todo list. Reconcile it now: mark each genuinely ' +\n 'finished item done (or skipped/failed with a reason) via write_todos, then continue any ' +\n 'remaining work.'\n\n/** todo 终态集合(与 todo-reminder.ts isIncompleteTodo 一致)。 */\nconst TODO_TERMINAL_STATUSES = new Set(['done', 'skipped', 'failed'])\n\n/** 两个 Map 的键值集合相等性(RFC-094 无进展判定:顺序无关、逐键比对)。 */\nfunction mapsEqual<K, V>(a: Map<K, V>, b: Map<K, V>): boolean {\n if (a.size !== b.size) return false\n for (const [k, v] of a) {\n if (!b.has(k) || b.get(k) !== v) return false\n }\n return true\n}\n\nexport interface TodoGateRunnerOptions {\n session: Session<AgentMessage>\n sessionId: string\n logger: Logger\n /** RFC-094:todo 硬闸配置(depth>0 时闸恒不启用,由调用方在构造前解析好)。 */\n todoContinuation: { enabled: boolean; max: number }\n depth: number\n publish: (event: Partial<{ [K in keyof AgentSessionEventMap]: Parameters<AgentSessionEventMap[K]> }>) => void\n /** 是否已被 abort(闸门循环轮间隙的中断捕获,调用方持有真源)。 */\n isAbortRequested: () => boolean\n /** 复用 AgentSession 的单轮 prompt 执行——闸门续跑轮与普通轮走同一管线。 */\n runPromptRound: (text: string, internal?: boolean) => Promise<{ cancelled: boolean; hadToolActivity: boolean }>\n}\n\n/**\n * RFC-094 todo 停机闭环硬闸(post-RFC-092 review F7 从 AgentSession 拆出的独立 concern)。\n *\n * 职责:incomplete>0 且 round<cap 且未 abort 时注入续跑轮,直至完成或无进展提前停。\n * 不负责:单轮 prompt 的执行细节——委托给注入的 runPromptRound(AgentSession 持有的完整管线)。\n */\nexport class TodoGateRunner {\n private readonly session: Session<AgentMessage>\n private readonly sessionId: string\n private readonly logger: Logger\n private readonly todoContinuation: { enabled: boolean; max: number }\n private readonly depth: number\n private readonly publish: TodoGateRunnerOptions['publish']\n private readonly isAbortRequested: () => boolean\n private readonly runPromptRound: TodoGateRunnerOptions['runPromptRound']\n\n constructor(options: TodoGateRunnerOptions) {\n this.session = options.session\n this.sessionId = options.sessionId\n this.logger = options.logger\n this.todoContinuation = options.todoContinuation\n this.depth = options.depth\n this.publish = options.publish\n this.isAbortRequested = options.isAbortRequested\n this.runPromptRound = options.runPromptRound\n }\n\n /** 当前 metadata todoList 的 id→status 快照(无进展判定基准:集合相等而非计数)。 */\n private snapshot(): Map<string | undefined, string | undefined> {\n return new Map((this.session.getTodoList() ?? []).map((t) => [t.id, t.status]))\n }\n\n private countIncomplete(): number {\n return (this.session.getTodoList() ?? []).filter(\n (t) => !TODO_TERMINAL_STATUSES.has(t.status ?? 'pending'),\n ).length\n }\n\n /** RFC-104 D4:todo 快照推进判定——≥1 项进入终态或 status 变化即 productive。 */\n private static hasStateProgress(\n before: Map<string | undefined, string | undefined>,\n after: Map<string | undefined, string | undefined>,\n ): boolean {\n return !mapsEqual(before, after)\n }\n\n /**\n * RFC-094 闸门循环 + RFC-104 D4 进展感知续跑:incomplete>0 且未 abort 时注入续跑轮。\n * 豁免:depth>0(sub-agent 由父会话编排,防嵌套循环)、enabled=false。\n *\n * RFC-104 D4(diminishing returns + 无自主活动时抑制续跑):\n * - **productive 轮不消耗 cap**:续跑轮结束后 todo 快照有推进(≥1 项状态变化)→\n * 该轮不计入 round(做得动就不掐)。cap 只约束**非生产轮**。\n * - **hardCap 兜底**:总轮数(含 productive)≤ max×HARD_CAP_FACTOR——防「每轮推进\n * 1 项但项无限增殖」的病态(todo 列表越做越长永不收敛)。\n * - **diminishing returns 显式计数**:连续 2 轮「有工具活动但零 todo 推进」(sync-nudge\n * 已单发过)→ 停。原逻辑 nudge 后一轮无变化即放行,收紧为显式 staleActivityRounds 计数\n * (nudge 轮本身算第 1 次,nudge 后仍无推进为第 2 次 → 停,语义与原实现等价但计数可见)。\n *\n * 无进展分派(快照相等时):\n * a) 零工具活动(模型真卡死/空转)→ 放行出循环(防弱模型烧 token,原语义);\n * b) 有工具活动但 todo 没动(干了活没记账)→ 定向 sync nudge 一次(单发,不占 cap);\n * nudge 后仍无推进 → diminishing 停。\n */\n async run(): Promise<void> {\n if (!this.todoContinuation.enabled || this.depth > 0) return\n\n /** RFC-104 D4:hardCap = max × 4——productive 轮免计 cap 的总量兜底(防 todo 增殖病态)。 */\n const HARD_CAP_FACTOR = 4\n const hardCap = this.todoContinuation.max * HARD_CAP_FACTOR\n\n let round = 0 // 非生产轮计数(吃 cap)\n let totalRounds = 0 // 全部续跑轮计数(吃 hardCap)\n let staleActivityRounds = 0 // 连续「有活动零推进」轮计数(diminishing returns)\n let syncNudged = false\n let nextPrompt = TODO_GATE_CONTINUE_PROMPT\n let before = this.snapshot()\n while (\n round < this.todoContinuation.max &&\n totalRounds < hardCap &&\n !this.isAbortRequested() &&\n this.countIncomplete() > 0\n ) {\n round++\n totalRounds++\n this.publish({\n 'prompt.continued': [\n {\n type: 'prompt.continued' as const,\n sessionId: this.sessionId,\n round: totalRounds,\n maxRounds: hardCap,\n incompleteCount: this.countIncomplete(),\n },\n ],\n })\n this.logger.info(\n { sessionId: this.sessionId, round, totalRounds, max: this.todoContinuation.max, hardCap },\n 'Todo gate: auto-continuing unfinished todos',\n )\n\n const res = await this.runPromptRound(nextPrompt, true)\n if (res.cancelled) return\n nextPrompt = TODO_GATE_CONTINUE_PROMPT\n\n const after = this.snapshot()\n if (TodoGateRunner.hasStateProgress(before, after)) {\n // RFC-104 D4:productive 轮退还 cap 配额(totalRounds 照计,hardCap 兜底)。\n round--\n staleActivityRounds = 0\n before = after\n continue\n }\n\n // 快照相等:无 todo 推进。\n if (res.hadToolActivity && !syncNudged) {\n // b) 有产出但没记账:下一轮改发定向对账指令(单发),不放行。\n // round-- :nudge 轮不占续跑配额(评审 F6)。staleActivityRounds=1(diminishing 第 1 计)。\n round--\n syncNudged = true\n staleActivityRounds = 1\n nextPrompt = TODO_GATE_SYNC_PROMPT\n this.logger.info(\n { sessionId: this.sessionId, round },\n 'Todo gate: tool activity without todo updates — nudging state reconciliation',\n )\n continue\n }\n if (res.hadToolActivity) {\n // nudge 已发过仍有活动零推进:diminishing 第 2 计 → 停(RFC-104 D4 显式计数)。\n staleActivityRounds++\n this.logger.info(\n { sessionId: this.sessionId, staleActivityRounds },\n 'Todo gate: diminishing returns (repeated activity without todo progress), stopping',\n )\n return\n }\n // a) 零动作:放行(原语义)。\n this.logger.info(\n { sessionId: this.sessionId, round },\n 'Todo gate: no progress between rounds, stopping early',\n )\n return\n }\n }\n}\n","import { forceMemoryTransform, type MemoryPort } from './memory-transform'\n\nimport type { Logger } from '@x-otto/shared'\nimport type { Session } from '@x-otto/session'\nimport type { AgentMessage, PersistencePort } from '@x-otto/agent'\nimport type { HookRegistry } from '@x-otto/hooks'\nimport type { AgentSessionEventMap } from './types'\n\ntype PublishFn = (\n event: Partial<{ [K in keyof AgentSessionEventMap]: Parameters<AgentSessionEventMap[K]> }>,\n) => void\n\nexport interface SessionCompactionDeps {\n session: Session<AgentMessage>\n sessionId: string\n hookRegistry: HookRegistry\n logger: Logger\n publish: PublishFn\n}\n\n/**\n * 引擎侧已算好摘要的压缩写回(RFC-199 D3 自 AgentSession 提取,行为零变化)。\n * compaction.before/after hook + compaction.start/end 事件语义原样保留。\n */\nexport async function compactSession(\n deps: SessionCompactionDeps,\n summary: string,\n compactedCount: number,\n): Promise<void> {\n const { session, sessionId, hookRegistry, logger, publish } = deps\n const messageCount = session.messages().length\n\n await hookRegistry.emit('compaction.before', { sessionId, messageCount })\n\n publish({\n 'compaction.start': [{ type: 'compaction.start' as const, sessionId, messageCount }],\n })\n logger.info({ sessionId, count: compactedCount }, 'Session compacting')\n\n session.compact(summary, compactedCount)\n\n const retainedCount = session.messages().length\n\n await hookRegistry.emit('compaction.after', { sessionId, retainedCount })\n\n publish({\n 'compaction.end': [{ type: 'compaction.end' as const, sessionId, retainedCount, summary }],\n })\n logger.info(\n { sessionId, retained: retainedCount, summaryChars: summary.length },\n 'Session compaction finished',\n )\n}\n\n/**\n * 按需强制压缩(/compact 命令入口)——复用引擎自动/反应式同款路径:\n * memory.process({force:true}) 生成真实摘要 → recordCompaction 写回会话树(与自动压缩同一写回点,\n * buildContext/重启原样恢复)。emit compaction.start/end 供 TUI 显示压缩进度。\n *\n * 注:压缩只缩小 buildContext()(送 LLM 的视图),messages()(完整转录)刻意保留——\n * 用户可见的滚动历史不被抹除。before/after 量的是 LLM 视图的消息数。\n * 返回 compacted=false 表示无可压缩内容(消息太少 / memory 未启用)。\n */\nexport async function compactSessionNow(\n deps: SessionCompactionDeps & { memory?: MemoryPort; persistence: PersistencePort },\n signal?: AbortSignal,\n): Promise<{ compacted: boolean; before: number; after: number }> {\n const { session, sessionId, hookRegistry, logger, memory, persistence } = deps\n const ctx = session.buildContext()\n const before = ctx.messages.length\n\n if (!memory || signal?.aborted) {\n return { compacted: false, before, after: before }\n }\n\n await hookRegistry.emit('compaction.before', { sessionId, messageCount: before })\n logger.info({ sessionId, messageCount: before }, 'Session manual compaction (forced)')\n\n // 复用引擎反应式压缩的副作用单源(memory.process({force}) → recordCompaction 写回会话树,与\n // 自动/413 反应式压缩同一写回点)。不传 hooks——manual /compact 的 before/after emit 是无条件\n // 的 UX 进度(即便无可压缩内容也发),与 helper 内「仅确已压缩时」的 compaction.* 语义不同,故\n // emit 留在本函数。sink.compacted 即「确已写回」信号,比 result.compacted 更贴近用户可见结果。\n const sink = { compacted: false }\n await forceMemoryTransform(memory, ctx.messages as AgentMessage[], sessionId, signal, {\n persistence,\n sink,\n })\n\n const after = session.buildContext().messages.length\n await hookRegistry.emit('compaction.after', { sessionId, retainedCount: after })\n logger.info({ sessionId, retainedCount: after }, 'Session manual compaction finished')\n\n return { compacted: sink.compacted, before, after }\n}\n","import type { Agent } from '@x-otto/agent'\nimport type { AgentSessionEventMap } from './types'\n\ntype PublishFn = (\n event: Partial<{ [K in keyof AgentSessionEventMap]: Parameters<AgentSessionEventMap[K]> }>,\n) => void\n\n/**\n * Agent 引擎事件 → AgentSession 事件面的纯投影转发(RFC-199 D3 自构造函数提取,行为零变化):\n * max.turns(RFC-104 D5 预算触顶三态)/ prompt.budget(RFC-104 D3/D5 token/时间预算)/\n * progress.stall(RFC-203 D2 触顶前早期停滞检出)/ image.vision-delegate(RFC-181 D6 视觉委托结果)/\n * compaction.reactive(终局 review 2026-07-21 补齐:上下文溢出反应式压缩,此前零 UI 投影)/\n * image.degradation(终局 review 2026-07-21 补齐:图片降级兜底,此前零 UI 投影)。\n * 返回 unsubscribe 列表,由 AgentSession 纳入 agentUnsubs 统一清理。\n * steered 标记的副作用经 onBudgetSteered 回调透出(保持 budgetSteeredThisPrompt 语义)。\n */\nexport function wireAgentBudgetAndVisionForwarding(\n agent: Agent,\n sessionId: string,\n publish: PublishFn,\n onBudgetSteered: () => void,\n): Array<() => void> {\n return [\n agent.on('max.turns', (e) => {\n if (e.steered) onBudgetSteered()\n publish({\n 'budget.notice': [\n {\n type: 'budget.notice' as const,\n sessionId,\n turnIndex: e.turnIndex,\n maxTurns: e.maxTurns,\n extended: e.extended,\n ...(e.steered !== undefined ? { steered: e.steered } : {}),\n ...(e.progressDenied !== undefined ? { progressDenied: e.progressDenied } : {}),\n },\n ],\n })\n }),\n agent.on('prompt.budget', (e) => {\n if (e.steered) onBudgetSteered()\n publish({\n 'prompt.budget.notice': [\n {\n type: 'prompt.budget.notice' as const,\n sessionId,\n turnIndex: e.turnIndex,\n dimension: e.dimension,\n used: e.used,\n budget: e.budget,\n steered: e.steered,\n },\n ],\n })\n }),\n agent.on('progress.stall', (e) => {\n // RFC-203 D2:接线时发现的真实语义冲突——不复用 onBudgetSteered。该回调驱动\n // `budgetSteeredThisPrompt`,print 模式消费方(run-modes.ts)据此渲染写死文案\n // \"已达工具回合上限\"(max-tool-turns 专属措辞);停滞检出不是触顶,误报会让用户\n // 以为回合数耗尽。两种收尾原因需要独立信号——本事件只经 progress.stall.notice\n // 投影,interactive 模式的 toast 消费方(RFC-203 M4)按事件类型精确区分文案。\n publish({\n 'progress.stall.notice': [\n {\n type: 'progress.stall.notice' as const,\n sessionId,\n turnIndex: e.turnIndex,\n windowTurns: e.windowTurns,\n maxRepeat: e.maxRepeat,\n },\n ],\n })\n }),\n agent.on('tool.call.mismatch', (e) => {\n // RFC-232 D4:工具调用协议违约——投影为 notice 供 TUI toast(避免无声中断)。\n // 对齐 progress.stall.notice 纹理:TUI 经原始事件直接消费、按 outcome 精确区分文案。\n publish({\n 'tool.call.mismatch.notice': [\n {\n type: 'tool.call.mismatch.notice' as const,\n sessionId,\n turnIndex: e.turnIndex,\n outcome: e.outcome,\n retryCount: e.retryCount,\n },\n ],\n })\n }),\n agent.on('image.vision-delegate', (e) => {\n publish({\n 'image.vision-delegate': [\n {\n type: 'image.vision-delegate' as const,\n sessionId,\n attemptedCount: e.attemptedCount,\n describedCount: e.describedCount,\n failedCount: e.failedCount,\n },\n ],\n })\n }),\n agent.on('compaction.reactive', (e) => {\n publish({\n 'compaction.reactive.notice': [\n {\n type: 'compaction.reactive.notice' as const,\n sessionId,\n messagesBefore: e.messagesBefore,\n messagesAfter: e.messagesAfter,\n },\n ],\n })\n }),\n agent.on('image.degradation', (e) => {\n publish({\n 'image.degradation.notice': [\n {\n type: 'image.degradation.notice' as const,\n sessionId,\n degradedCount: e.degradedCount,\n trigger: e.trigger,\n },\n ],\n })\n }),\n ]\n}\n","/**\n * tool-result-store.ts — RFC-093:工具结果溢出落盘设施\n *\n * 超预算的工具结果写入项目本地 `.otto/tool-results/<sessionId>/<toolCallId>.txt`,\n * 上下文只留 preview + 路径占位符,模型经现有 read 工具按需恢复。\n *\n * 设计契约(RFC-093 §3 D3、§6 规则2):\n * - 项目本地目录(.otto/ 在项目根内)——read 工具路径约束天然放行,与 remote storage 解耦。\n * - fail-open:任何写盘失败返回 undefined,调用方回退现状截断,绝不阻塞工具链。\n * - 100MB 软上限:超限驱逐最旧文件(恢复 fail → 模型重跑工具,与 prune 丢弃等价)。\n */\n\nimport { mkdir, writeFile, readdir, stat, unlink, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { isShadowModeEnabled } from '@x-otto/env'\n\nconst MAX_STORE_BYTES = 100 * 1024 * 1024\n\n/** toolCallId 消毒:只保留安全字符,防路径注入。 */\nfunction sanitizeId(id: string): string {\n return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128) || 'unnamed'\n}\n\nexport class ToolResultStore {\n private readonly rootDir: string\n\n /** @param projectOttoDir 项目 .otto 目录(如 `<cwd>/.otto`)。 */\n constructor(projectOttoDir: string) {\n this.rootDir = join(projectOttoDir, 'tool-results')\n }\n\n /** 会话子目录路径。 */\n private sessionDir(sessionId: string): string {\n return join(this.rootDir, sanitizeId(sessionId))\n }\n\n /**\n * 溢出写盘。成功返回文件绝对路径(进占位符),失败返回 undefined(调用方 fail-open)。\n */\n async spill(sessionId: string, toolCallId: string, text: string): Promise<string | undefined> {\n // RFC-345 §D8:影子态不溢写工具结果到项目内 .otto/tool-results/——返回 undefined\n // 让调用方 fail-open 回退到现状截断(不落盘,模型拿截断结果,与 prune 丢弃等价)。\n if (isShadowModeEnabled()) {\n return undefined\n }\n try {\n const dir = this.sessionDir(sessionId)\n await mkdir(dir, { recursive: true })\n const path = join(dir, `${sanitizeId(toolCallId)}.txt`)\n await writeFile(path, text, 'utf-8')\n // 软上限驱逐(best-effort,不阻塞返回)\n void this.evictIfOverBudget().catch(() => {})\n return path\n } catch {\n return undefined\n }\n }\n\n /** 会话结束清理:删除该会话的全部溢出文件。 */\n async cleanupSession(sessionId: string): Promise<void> {\n try {\n await rm(this.sessionDir(sessionId), { recursive: true, force: true })\n } catch {\n /* best-effort */\n }\n }\n\n /** 全 store 字节数超 100MB 时驱逐最旧文件(按 mtime)。 */\n async evictIfOverBudget(): Promise<void> {\n const files: Array<{ path: string; mtimeMs: number; size: number }> = []\n let total = 0\n\n let sessionDirs: string[]\n try {\n sessionDirs = await readdir(this.rootDir)\n } catch {\n return\n }\n\n for (const sub of sessionDirs) {\n const dir = join(this.rootDir, sub)\n let names: string[]\n try {\n names = await readdir(dir)\n } catch {\n continue\n }\n for (const name of names) {\n const path = join(dir, name)\n try {\n const s = await stat(path)\n if (s.isFile()) {\n files.push({ path, mtimeMs: s.mtimeMs, size: s.size })\n total += s.size\n }\n } catch {\n /* skip */\n }\n }\n }\n\n if (total <= MAX_STORE_BYTES) return\n\n files.sort((a, b) => a.mtimeMs - b.mtimeMs)\n for (const f of files) {\n if (total <= MAX_STORE_BYTES) break\n try {\n await unlink(f.path)\n total -= f.size\n } catch {\n /* skip */\n }\n }\n }\n}\n","/**\n * tool-result-store-global.ts — RFC-093:ToolResultStore 全局单例\n *\n * AgentSession 构造 spill 闭包时引用。懒初始化(首次 spill 时基于 cwd 解析 .otto 目录)——\n * 避免模块加载期做 fs 探测。\n */\n\nimport { join } from 'node:path'\n\nimport { ToolResultStore } from './tool-result-store'\n\nlet instance: ToolResultStore | undefined\n\nfunction store(): ToolResultStore {\n instance ??= new ToolResultStore(join(process.cwd(), '.otto'))\n return instance\n}\n\nexport const globalToolResultStore = {\n spill(sessionId: string, toolCallId: string, text: string): Promise<string | undefined> {\n return store().spill(sessionId, toolCallId, text)\n },\n cleanupSession(sessionId: string): Promise<void> {\n return store().cleanupSession(sessionId)\n },\n}\n","import { ENGINE_DEFAULTS } from '@x-otto/agent'\n\nexport const RUNTIME_DEFAULTS = {\n /** 工具轮次预算缺省(RFC-057 M89-01 单源):引用 agent 核心 ENGINE_DEFAULTS.maxToolTurns,\n * 不再硬编码——改其一全生效,消除散落 `?? 50` 漂移。 */\n maxToolTurns: ENGINE_DEFAULTS.maxToolTurns,\n} as const\n","/** 单个待决 promise + 其 resolver(HITL gate 的等待原语)。 */\ninterface Deferred<T> {\n resolve: (value: T) => void\n reject: (reason?: unknown) => void\n promise: Promise<T>\n}\n\nfunction createDeferred<T>(): Deferred<T> {\n let resolve!: (value: T) => void\n let reject!: (reason?: unknown) => void\n const promise = new Promise<T>((res, rej) => {\n resolve = res\n reject = rej\n })\n return { resolve, reject, promise }\n}\n\n/**\n * HITL 待决 gate 的生命周期单源(RFC-083 M4 · ARCH-083-06)。\n *\n * 持一组按 id 索引的待决 deferred,封装「注册 → settle 自动清理」+ 按 id resolve + 全部 reject。\n * 三个 gate(approval / grill / plan)各持一个 `PendingGate` 实例——把原先散在 AgentSession 的 deferred\n * 样板(创建 / 存 / finally 清)+ `rejectPendingGates` 手工逐字段 reject 收敛为统一原语。\n *\n * **非交互 fallback 与 `*.required`/`*.resolved` 事件 publish 仍由各 request/resolve 方法持有**——\n * 本类只抽共性生命周期,不强统一三者的语义差异(§7 放弃「为抽象而抽象」的统一泛型)。\n */\nexport class PendingGate<T> {\n private readonly pending = new Map<string, Deferred<T>>()\n\n /** 注册一个待决 gate,返回其 promise;settle(resolve/reject)后自动从表移除。 */\n open(id: string): Promise<T> {\n const deferred = createDeferred<T>()\n this.pending.set(id, deferred)\n return deferred.promise.finally(() => {\n this.pending.delete(id)\n })\n }\n\n /** 按 id resolve;命中返 true(调用方据此 publish `*.resolved`),未命中(已 settle / 非当前)返 false。 */\n resolve(id: string, value: T): boolean {\n const deferred = this.pending.get(id)\n if (!deferred) return false\n deferred.resolve(value)\n return true\n }\n\n /** 中止时 reject 全部待决(会话 abort / dispose)。 */\n rejectAll(reason: Error): void {\n for (const deferred of this.pending.values()) {\n deferred.reject(reason)\n }\n this.pending.clear()\n }\n}\n","import { randomUUID } from 'node:crypto'\n\nimport { PendingGate } from './pending-gate'\n\nimport type { Logger } from '@x-otto/shared'\nimport type { ApprovalRisk } from '@x-otto/agent'\nimport type { AgentSessionEventMap, GrillQuestion, GrillAnswer } from './types'\n\n/** publish 形状与 EventBus.publish 对齐(AgentSession 构造时以闭包注入,避免反向依赖)。 */\ntype PublishFn = (\n event: Partial<{ [K in keyof AgentSessionEventMap]: Parameters<AgentSessionEventMap[K]> }>,\n) => void\n\nexport interface SessionInteractionGatesDeps {\n sessionId: () => string\n interactive: boolean\n logger: Logger\n publish: PublishFn\n}\n\n/**\n * HITL 三 gate(approval / grill / plan)的请求-裁决生命周期(RFC-199 D3 自 AgentSession 提取)。\n *\n * 职责:`*.required` 事件发布 + PendingGate 等待 + `*.resolved` 事件发布 + 非交互 fallback。\n * 不持有会话状态——sessionId 经 getter 注入,publish 经闭包注入;AgentSession 组合持有本类,\n * 公共 API(requestApproval 等)保持在 AgentSession 上委托,消费方(coding/cli)零改动。\n */\nexport class SessionInteractionGates {\n private readonly approvalGate = new PendingGate<boolean>()\n private readonly grillGate = new PendingGate<GrillAnswer>()\n private readonly planGate = new PendingGate<{ action: string; feedback?: string }>()\n\n constructor(private readonly deps: SessionInteractionGatesDeps) {}\n\n async requestApproval(\n toolName: string,\n args: Record<string, unknown>,\n description: string,\n risk?: ApprovalRisk,\n ): Promise<boolean> {\n const { sessionId, interactive, logger, publish } = this.deps\n if (!interactive) {\n logger.warn(\n { sessionId: sessionId(), toolName },\n 'Approval requested in a non-interactive session; denying (fail-closed, no human to respond)',\n )\n return false\n }\n\n const id = randomUUID()\n const promise = this.approvalGate.open(id)\n\n publish({\n 'approval.required': [\n {\n type: 'approval.required' as const,\n sessionId: sessionId(),\n id,\n toolName,\n arguments: args,\n description,\n risk,\n },\n ],\n })\n\n logger.info({ sessionId: sessionId(), toolName, approvalId: id }, 'Approval requested')\n\n return promise\n }\n\n resolveApproval(approvalId: string, approved: boolean): void {\n const { sessionId, logger, publish } = this.deps\n if (this.approvalGate.resolve(approvalId, approved)) {\n publish({\n 'approval.resolved': [\n { type: 'approval.resolved' as const, sessionId: sessionId(), id: approvalId, approved },\n ],\n })\n logger.info({ sessionId: sessionId(), approvalId, approved }, 'Approval resolved')\n }\n }\n\n async requestGrill(q: Omit<GrillQuestion, 'id'>): Promise<GrillAnswer> {\n const { sessionId, interactive, logger, publish } = this.deps\n if (!interactive) {\n return {\n answer: q.recommendation,\n acceptedRecommendation: true,\n resolvedBy: 'auto_recommendation',\n }\n }\n\n const requestId = randomUUID()\n const question: GrillQuestion = { ...q, id: requestId }\n const promise = this.grillGate.open(requestId)\n\n publish({\n 'ask.user.required': [\n { type: 'ask.user.required' as const, sessionId: sessionId(), requestId, question },\n ],\n })\n\n logger.info({ sessionId: sessionId(), requestId, askedBy: q.askedBy }, 'Grill requested')\n\n return promise\n }\n\n resolveAskUser(requestId: string, answer: GrillAnswer | null): void {\n const { sessionId, logger, publish } = this.deps\n if (answer && this.grillGate.resolve(requestId, answer)) {\n publish({\n 'ask.user.resolved': [\n { type: 'ask.user.resolved' as const, sessionId: sessionId(), requestId },\n ],\n })\n logger.info({ sessionId: sessionId(), requestId }, 'Grill resolved')\n }\n }\n\n async requestPlanApproval(planContent: string): Promise<{ action: string; feedback?: string }> {\n const { sessionId, logger, publish } = this.deps\n const requestId = randomUUID()\n const promise = this.planGate.open(requestId)\n\n publish({\n 'plan.approval.required': [\n { type: 'plan.approval.required' as const, sessionId: sessionId(), requestId, planContent },\n ],\n })\n\n logger.info({ sessionId: sessionId(), requestId }, 'Plan approval requested')\n\n return promise\n }\n\n resolvePlanApproval(requestId: string, action: string, feedback?: string): void {\n const { sessionId, logger, publish } = this.deps\n if (this.planGate.resolve(requestId, { action, feedback })) {\n publish({\n 'plan.approval.resolved': [\n { type: 'plan.approval.resolved' as const, sessionId: sessionId(), requestId, action },\n ],\n })\n logger.info({ sessionId: sessionId(), action, requestId }, 'Plan approval resolved')\n }\n }\n\n /** 中止时 reject 全部待决(会话 abort / dispose)。 */\n rejectAll(reason: Error): void {\n this.approvalGate.rejectAll(reason)\n this.grillGate.rejectAll(reason)\n this.planGate.rejectAll(reason)\n }\n}\n","import { EventBus } from '@x-otto/shared'\nimport { createToolHookExecutor } from './hooks'\nimport { deriveSessionTitle } from './derive-title'\nimport { SessionTitleManager } from './session-title-manager'\nimport { SessionPersistenceBridge } from './session-persistence-bridge'\nimport { TodoGateRunner, TODO_GATE_CONTINUE_PROMPT, TODO_GATE_SYNC_PROMPT } from './todo-gate-runner'\nimport { compactSession, compactSessionNow } from './agent-session-compaction'\nimport { wireAgentBudgetAndVisionForwarding } from './agent-session-event-forwarding'\nimport { type MemoryPort } from './memory-transform'\nimport { globalToolResultStore } from './tool-result-store-global'\nimport { RUNTIME_DEFAULTS } from '../defaults'\nimport { CostTracker, computeCacheHitRatio, type AssistantMessage } from '@x-otto/ai'\nimport { createHookRegistry, provisionBaselineSecurity } from '@x-otto/hooks'\nimport { SESSION_COMPACTION_TIMEOUT_MS } from '@x-otto/session-contract'\nimport { createLogger, type Logger } from '@x-otto/shared'\nimport {\n createAgent,\n prompt,\n extractToolResultText,\n stripAnsi,\n ENGINE_DEFAULTS,\n type Agent,\n type AgentMessage,\n type AgentTool,\n type ToolCallEvent,\n type ToolResult,\n type PersistencePort,\n type TraceRecorder,\n type ClockPort,\n type ApprovalRisk,\n type NewMessagesAccumulator,\n type DescribeImagesPort,\n} from '@x-otto/agent'\nimport type { HookRegistry } from '@x-otto/hooks'\nimport type { Session } from '@x-otto/session'\nimport type { CostSummary, Message, Model, ModelResolution, StreamEvent, ThinkingLevel } from '@x-otto/ai'\nimport type { Devtools } from '@x-otto/devtools'\nimport type {\n AgentSessionOptions,\n AgentSessionEvent,\n AgentSessionEventMap,\n AgentSessionEventType,\n AgentSessionSubscriber,\n GrillQuestion,\n GrillAnswer,\n PromptRefresh,\n PromptOptions,\n} from './types'\n\nimport { SessionInteractionGates } from './agent-session-gates'\n\nexport function defaultMaxToolTurnExtensions(depth: number, transient?: boolean): number {\n return depth > 0 || transient ? 0 : 3\n}\n\n/** tool.call.end.resultText 截断——UI 预览量级,非模型上下文(那条路走 clampWithSpill)。 */\nfunction truncateResultText(text: string, maxChars: number): string | undefined {\n if (!text) return undefined\n return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text\n}\n\n/**\n * RFC-094 D4:todo 硬闸续跑配置解析(env,语义延续 RFC-042 D5-r)——\n * 默认开(opt-out):显式 `OTTO_TODO_CONTINUE=0/off/false/no` 才关;\n * cap 默认 3,允许 0(=不续),NaN/Infinity/负数/非法 → 回落 3。\n */\nexport function resolveTodoContinuationConfig(env: {\n OTTO_TODO_CONTINUE?: string\n OTTO_TODO_CONTINUE_MAX?: string\n}): { enabled: boolean; max: number } {\n const raw = (env.OTTO_TODO_CONTINUE ?? '').toLowerCase()\n const enabled = !(raw === '0' || raw === 'off' || raw === 'false' || raw === 'no')\n const n = Math.trunc(Number(env.OTTO_TODO_CONTINUE_MAX ?? 3))\n const max = Number.isFinite(n) && n >= 0 ? n : 3\n return { enabled, max }\n}\n\n/**\n * RFC-094 续跑触发提示语——实现下沉至 todo-gate-runner.ts(post-RFC-092 review F7),\n * 此处 re-export 保持既有消费方(如 doc-sync-reminder.ts 注释、测试)的 import 路径不变。\n */\nexport { TODO_GATE_CONTINUE_PROMPT, TODO_GATE_SYNC_PROMPT }\n\nexport class AgentSession extends EventBus<AgentSessionEventMap> {\n readonly session: Session<AgentMessage>\n readonly workspaceDir: string\n readonly workspaceId?: string\n\n private agent: Agent\n private disposed = false\n private promptGate = false\n\n /** RFC-078 M134:会话标题生成(post-RFC-092 review F7 拆出为独立 concern)。 */\n private readonly titleManager: SessionTitleManager\n /**\n * 本次 prompt 是否已真正进入过 in-flight run(见过 status→streaming)。用于把 promptGate 期细分为\n * 「prep 期(run 即将来)」vs「收尾期(run 已结束,钩子 chat.message.after/session.idle/session.error\n * 仍在 await)」——两者 agent.status 相同(上一/本回合终态)、isExecuting 都为 false,无法用状态区分。\n * abort 只在 prep 期才允许 arm pendingAbort;收尾期 arm 会毒化下一回合(completed/error/aborted\n * settling 静默黑洞)。每次 prompt 入口重置,status→streaming 时置 true。\n */\n private runStartedThisPrompt = false\n\n /**\n * RFC-194 D1:in-flight prompt 生命周期 Promise——clearContext() busy 守卫的 settle\n * 依赖。prompt() 入口(isBusy 守卫后)赋值,finally 清除;空闲时 undefined。\n */\n private currentPrompt: Promise<void> | undefined\n\n /** RFC-324 D1 M2:逃生阀压缩的 pending Promise——requestEscapeValveCompaction 写回,\n * runPromptRound 开头 await。解决 fire-and-forget 压缩不保证收敛的问题(缺口 A)。\n * 仅 gate 逃生阀压缩(字节维度);主循环压缩已在 prompt 管线内同步 await。 */\n compactionPending: Promise<{ compacted: boolean; before: number; after: number }> | undefined\n\n /** RFC-324 D1/R5:turn-gate 超时兜底的惰性读数(见 AgentSessionOptions 同名字段)。 */\n private readonly getCompactionTimeoutMs?: () => number | undefined\n\n public model: Model\n /** 模型决策溯源(哪层选中 + 拒了谁)——供 task-runner 透传观测层。 */\n public modelResolution?: ModelResolution\n public tools: AgentTool[]\n public systemPrompt: string\n /** 会话恒定的 volatile system 尾段种子(append 子代理角色块等);透传至每轮 PromptParams.systemTail。 */\n public systemTail?: string[]\n public agentName: string\n public depth: number\n public maxToolTurns: number\n public maxToolTurnExtensions: number\n /** RFC-104 D3:per-prompt 预算(fork 拷贝面;depth>0 构造时已被剥除)。 */\n public promptOutputTokenBudget?: number\n public promptWallClockBudgetMs?: number\n public thinkingLevel: ThinkingLevel\n public promptRefresh?: PromptRefresh\n\n private logger: Logger\n private hookRegistry: HookRegistry\n private devtools?: Devtools\n private memory?: MemoryPort\n private readonly recorder?: TraceRecorder\n private readonly describeImages?: DescribeImagesPort\n /** RFC-145 D3:nondet batch 冲洗回调。 */\n private readonly nondetFlush?: () => void\n private readonly clock?: ClockPort\n\n /** RFC-337 D2:steer 消息 id 单调计数器(会话内唯一,供 ESC 撤回按序移除)。 */\n private steerSeq = 0\n\n private readonly persistence: PersistencePort\n\n /**\n * RFC-336 D2:输入边界落盘回调(由 `PersistenceSync.track()` 注入,同\n * `setReplacementStripEligibility` 的既有模式)。未注入 = 该会话不做输入边界落盘\n * (transient 子会话、in-memory 后端、snapshot 远端后端均属此列)。\n */\n private inputBoundaryFlusher?: () => void\n\n /** RFC-336 D2:供 SessionPool 侧注入输入边界落盘策略;见 `inputBoundaryFlusher`。 */\n setInputBoundaryFlusher(flush: () => void): void {\n this.inputBoundaryFlusher = flush\n }\n\n /** HITL 三 gate(approval/grill/plan)生命周期,RFC-199 D3 提取(agent-session-gates.ts)。 */\n private readonly gates: SessionInteractionGates\n private readonly interactive: boolean\n private readonly agentUnsubs: Array<() => void> = []\n private readonly costTracker = new CostTracker()\n /** RFC-204 D1/D2:会话级成本软预算阈值(美元);undefined = 不启用检测。 */\n private readonly sessionCostBudgetUSD?: number\n /** RFC-204 D2:本会话是否已越过成本预算并提醒过(一次性,session 生命周期内不重复)。 */\n private sessionCrossedCostBudget = false\n private lastTokenUsage: { input: number; output: number } | undefined\n /**\n * RFC-019 M1:本回合缓存命中率(R1 = cacheRead / totalInput)。\n * `null` = 供应商无缓存 API 或本回合无 usage 数据 —— 消费方(TUI summary 行)据此隐藏该字段,\n * 不展示可能永远是 0 的误导性数字(见 docs/rfc/RFC-019-cache-hit-rate-metrics.md)。\n */\n private lastCacheHitRatio: number | null = null\n\n /** RFC-094:todo 停机闭环硬闸(post-RFC-092 review F7 拆出为独立 concern)。 */\n private readonly todoGateRunner: TodoGateRunner\n /**\n * RFC-094 D4(评审细化):闸门循环轮间隙(isExecuting=false)的 abort 捕获——\n * 既有 abort() 守卫在间隙期是 no-op,此标记让闸门循环下一轮续跑前感知中断。\n */\n private abortRequested = false\n\n /**\n * RFC-180 D2:turn 边界软停止请求标志(与 abortRequested 同构但语义独立)。置位后\n * workLoop 在下一个安全的 turn 边界(onTurnPersist 已落盘)正常 return,不检查\n * followUp、不触发 todo 闸门续跑。区别于 abort:不打断 in-flight 执行,会话保持\n * 在正常可续跑状态(`Agent.status` 仍是 'completed',非 'aborted')。\n *\n * `isBusy` 判定本身不受本标志影响(正交状态)——暂停生效后 `promptGate` 复位为\n * false、`isExecuting` 为 false,`isBusy` 天然变回 false。这是 D6 判忙链路必须\n * 额外 OR `isPaused` 的原因,也是 D9 SessionPool 驱逐必须额外豁免 `isPaused` 的\n * 原因(否则暂停态会话在 isBusy 视角下与\"已空闲\"完全无法区分)。\n */\n private pauseRequested = false\n\n get id(): string {\n return this.session.id\n }\n\n get title(): string | undefined {\n return this.session.metadata().title\n }\n\n get status(): string {\n return this.agent.status\n }\n\n /** 置顶状态(/resume 列表排序权重,不影响是否可输入)。 */\n get pinned(): boolean {\n return this.session.metadata().pinned ?? false\n }\n\n /** 置顶分组名(RFC-158)。仅 pinned=true 时有意义;未置顶或默认组时为 undefined。 */\n get pinGroup(): string | undefined {\n return this.session.metadata().pinGroup\n }\n\n /**\n * 暴露底层 Agent 供观测层接入(只读;attachAgentObserver 用)。\n * task-runner 在跑 task_delegate/fork 子会话时据此把运行态接入 globalAgentObservability。\n */\n get coreAgent(): Agent {\n return this.agent\n }\n\n get isExecuting(): boolean {\n return this.agent.status === 'streaming' || this.agent.status === 'tool_executing'\n }\n\n /**\n * prompt 全生命周期忙判定。isExecuting 只覆盖 streaming/tool_executing,\n * 漏掉 promptRefresh/buildContext/hooks 等流式开始前的窗口(此时 promptGate 已置位);\n * 池驱逐(idle/LRU)必须用本判定,否则会 dispose 掉一个 prompt 进行中的会话。\n */\n get isBusy(): boolean {\n return this.promptGate || this.isExecuting\n }\n\n /**\n * RFC-090 M0:renderer pull 式获取 session 元数据,替代 session.ready 推事件(避免竞态)。\n * 返回纯数据对象,同进程直接调用;进程分离后(RFC-091)通过一次性请求/响应透传。\n */\n getSessionInfo(): {\n sessionId: string\n modelId: string\n modelName: string\n systemPromptLength: number\n tools: string[]\n } {\n return {\n sessionId: this.id,\n modelId: this.model.id,\n modelName: this.model.name,\n systemPromptLength: this.systemPrompt?.length ?? 0,\n tools: this.tools.map((t) => t.name),\n }\n }\n\n constructor(session: Session<AgentMessage>, options: AgentSessionOptions) {\n super()\n this.session = session\n\n // RFC-077 M3-B:直接构造(无注入 hookRegistry,非经 SessionPool)的裸 AgentSession 也\n // safe-by-default——下沉权限策略引擎 + 基线安全策略。幂等,已注入则由宿主 provision。\n const hookRegistry = options.hookRegistry ?? createHookRegistry({ preset: 'default' })\n if (!options.hookRegistry) {\n provisionBaselineSecurity(hookRegistry)\n }\n const logger =\n options.logger ??\n createLogger(`agent-session:${session.id}`, {\n level: 'info',\n destination: 'stdout',\n })\n\n this.getCompactionTimeoutMs = options.getCompactionTimeoutMs\n\n const promptRefresh = options.promptRefresh\n const systemPrompt = options.systemPrompt ?? ''\n const tools = options.tools ?? []\n const thinkingLevel = options.thinkingLevel ?? 'medium'\n const maxToolTurns = options.maxToolTurns ?? RUNTIME_DEFAULTS.maxToolTurns\n const depth = options.depth ?? 0\n const maxToolTurnExtensions =\n options.maxToolTurnExtensions ?? defaultMaxToolTurnExtensions(depth, options.transient)\n // RFC-104 D6/R3:token/时间预算不下发子 agent(depth>0 忽略——防经委托链逐层放大,\n // D47a 同款泄漏防御)。子 agent 用自身 profile 的 maxToolTurns。\n const promptOutputTokenBudget = depth === 0 ? options.promptOutputTokenBudget : undefined\n const promptWallClockBudgetMs = depth === 0 ? options.promptWallClockBudgetMs : undefined\n // RFC-204 D4:stallDetection 不做 depth 门控(与上方 token/时间预算不同)——后者门控\n // 是防止委托链逐层复合泄漏(RFC-104 D6/R3,父层剩余预算传给子代理会不合预期地叠加);\n // 停滞检测是每个 Agent 实例独立持有的本地环形窗口(StallDetector),子代理同样可能\n // 原地打转,且各自窗口互不干扰、不存在复合泄漏问题,故原样透传全部 depth。\n const stallDetection = options.stallDetection\n // RFC-104 D2(翻案 RFC-050 D5「默认关」,理由见 RFC-104 §3 D2):顶层默认开、\n // 子 agent 默认关(触顶硬停结果返回父层,与 D47a 深度收敛同构)。\n // env OTTO_BUDGET_STEER_WRAPUP=0/off 为顶层关闭逃生口;=1/on 可强制开(含子 agent)。\n const steerEnv = process.env.OTTO_BUDGET_STEER_WRAPUP\n const budgetSteerWrapUp =\n options.budgetSteerWrapUp ??\n (steerEnv === '1' || steerEnv === 'on'\n ? true\n : steerEnv === '0' || steerEnv === 'off'\n ? false\n : depth === 0)\n const workspaceDir = options.workspaceDir ?? process.cwd()\n\n const { model, devtools, agentName } = options\n\n this.model = model\n this.modelResolution = options.modelResolution\n this.tools = tools\n this.systemPrompt = systemPrompt\n this.systemTail = options.systemTail\n this.thinkingLevel = thinkingLevel\n this.maxToolTurns = maxToolTurns\n this.maxToolTurnExtensions = maxToolTurnExtensions\n this.promptOutputTokenBudget = promptOutputTokenBudget\n this.promptWallClockBudgetMs = promptWallClockBudgetMs\n this.hookRegistry = hookRegistry\n this.agentName = agentName ?? 'agent'\n this.depth = depth\n this.interactive = options.interactive ?? false\n this.devtools = devtools\n this.memory = options.memory\n this.recorder = options.recorder\n this.describeImages = options.describeImages\n this.nondetFlush = options.nondetFlush\n this.clock = options.clock\n this.logger = logger\n this.workspaceDir = workspaceDir\n // RFC-204 D6:与 promptOutputTokenBudget 同款 depth 门控——per-session 成本预算是\n // per-prompt 预算的姊妹概念,同样不下发子 agent(防经委托链逐层复合泄漏,理由同 RFC-104\n // D6/R3:子代理各自独立预算才不会被父层剩余额度污染)。\n this.sessionCostBudgetUSD = depth === 0 ? options.sessionCostBudgetUSD : undefined\n this.workspaceId = options.workspaceId\n this.promptRefresh = promptRefresh\n this.gates = new SessionInteractionGates({\n sessionId: () => this.id,\n interactive: this.interactive,\n logger,\n publish: (event) => this.publish(event),\n })\n this.titleManager = new SessionTitleManager({\n session,\n stream: options.stream,\n titleModel: options.titleModel,\n })\n this.todoGateRunner = new TodoGateRunner({\n session,\n sessionId: session.id,\n logger,\n todoContinuation: options.todoContinuation ?? resolveTodoContinuationConfig(process.env),\n depth,\n publish: (event) => this.publish(event),\n isAbortRequested: () => this.abortRequested,\n runPromptRound: (text, internal) => this.runPromptRound(text, internal),\n })\n\n this.agent = createAgent({\n stream: options.stream,\n logger,\n maxToolTurns,\n maxToolTurnExtensions,\n budgetSteerWrapUp,\n promptOutputTokenBudget,\n promptWallClockBudgetMs,\n stallDetection,\n agentName: this.agentName,\n sessionId: session.id,\n depth: this.depth,\n toolHookExecutor: createToolHookExecutor({\n hookRegistry,\n agentName: this.agentName,\n sessionId: session.id,\n }),\n debug: devtools?.debugger,\n recorder: this.recorder,\n nondetFlush: this.nondetFlush,\n clock: this.clock,\n approval: (toolName, args, reason, risk) => this.requestApproval(toolName, args, reason, risk),\n // RFC-093:工具结果溢出落盘(sessionId 在此闭包捕获,agent 包零存储感知)\n spill: (toolCallId, text) => globalToolResultStore.spill(session.id, toolCallId, text),\n // RFC-230:顺带修复既有断链——此前本构造函数从未把 options.retryAccelerate 透传给\n // createAgent(),导致 TUI「立即重试」加速信号(retryAccelerate.skip)从 coding 层\n // 经 runtime 层时被静默丢弃,agent.ts 收到的 config.retryAccelerate 恒为 undefined。\n // 这是顺带修复的既有 bug,非本 RFC 新增能力(见 RFC-230 §1.5)。\n retryAccelerate: options.retryAccelerate,\n streamRetry: options.streamRetry,\n networkDisconnectRetry: options.networkDisconnectRetry,\n })\n\n this.agentUnsubs.push(\n this.agent.on('status.change', ({ to }) => {\n if (to === 'streaming') this.runStartedThisPrompt = true\n }),\n )\n // 预算/视觉委托纯投影转发——实现在 agent-session-event-forwarding.ts(RFC-104/181 语义原样)。\n this.agentUnsubs.push(\n ...wireAgentBudgetAndVisionForwarding(\n this.agent,\n this.id,\n (event) => this.publish(event),\n () => {\n this.budgetSteeredThisPrompt = true\n },\n ),\n )\n this.agentUnsubs.push(this.agent.on('stream.event', this.onStreamEvent))\n this.agentUnsubs.push(this.agent.on('turn.start', this.onTurnStart))\n this.agentUnsubs.push(this.agent.on('turn.end', this.onTurnEnd))\n this.agentUnsubs.push(this.agent.on('tool.call.start', this.onToolCallStart))\n this.agentUnsubs.push(this.agent.on('tool.call.end', this.onToolCallEnd))\n\n // RFC-337 D3:steer 消息被模型消费(drain 进本回合上下文)时透出 steer.consumed,\n // cli 侧据此把对应的待定 steer echo 转正 commit(此后不再可撤回)。只对携带 uuid 的\n // steer 消息发信号(budget 收尾等引擎内部注入的 steering 无 uuid,不需要 UI 转正)。\n this.agentUnsubs.push(\n this.agent.on('steering.injected', ({ messages }) => {\n for (const m of messages) {\n const id = (m as { uuid?: string }).uuid\n if (id) {\n this.publish({ 'steer.consumed': [{ type: 'steer.consumed' as const, sessionId: this.id, id }] })\n }\n }\n }),\n )\n\n // RFC-118 S3:PersistencePort 实现从内联闭包提取为独立类(session-persistence-bridge.ts)——\n // publish 依赖显式声明,recordCompaction/notifyPrune 的副作用逻辑可脱离 AgentSession 单测。\n this.persistence = new SessionPersistenceBridge({\n session: this.session,\n sessionId: this.id,\n publish: (event) => this.publish(event),\n // RFC-336 D2:输入边界落盘请求转发。真正的落盘策略由 SessionPool 侧的\n // PersistenceSync 在 track() 时经 setInputBoundaryFlusher 注入(同\n // setReplacementStripEligibility 的既有注入模式);未注入时为 no-op,\n // 退回改动前行为(首个 turn 后才落库)。\n requestInputBoundaryFlush: () => this.inputBoundaryFlusher?.(),\n })\n\n this.logger.info({ sessionId: this.id, model: this.model.id }, 'Session initialized')\n }\n\n /**\n * 由 SessionPool / 宿主类在 session.created hook 执行完毕后调用。\n * 此时 EventBridge 已完成订阅,session.start 事件可被正确接收。\n */\n notifyCreated(): void {\n this.publish({ 'session.start': [{ type: 'session.start' as const, sessionId: this.id }] })\n }\n\n onStreamEvent = async ({ event }: { event: StreamEvent }): Promise<void> => {\n this.publish({\n 'stream.event': [\n {\n type: 'stream.event' as const,\n sessionId: this.id,\n event,\n },\n ],\n })\n\n if (event.type === 'start') {\n this.publish({\n 'streaming.start': [\n { type: 'streaming.start' as const, sessionId: this.id, model: this.model.id },\n ],\n })\n\n // RFC-143 M0:fire-and-forget——stream.start 是流式热路径,挂起的插件 hook 不应让\n // 用户看到无响应的转圈动画。实际流式数据(streaming.start 事件)已在上方 publish。\n void this.hookRegistry.emit('stream.start', {\n sessionId: this.id,\n model: this.model.id,\n })\n } else if (event.type === 'done') {\n this.publish({\n 'streaming.end': [\n {\n type: 'streaming.end' as const,\n sessionId: this.id,\n model: this.model.id,\n stopReason: event.reason ?? 'end_turn',\n },\n ],\n })\n\n await this.hookRegistry.emit('stream.end', {\n sessionId: this.id,\n model: this.model.id,\n stopReason: event.reason ?? 'end_turn',\n // RFC-129 D6:Monitor turn.completed 投影需要真实 token 数据源。取本次 done 事件自带的\n // event.message.usage(即时数据,非会话级 this.lastTokenUsage——后者要等 promptUsage()\n // 在 runPromptRound 末尾统一更新才反映最新值,工具调用循环内多次 stream.end 触发时会\n // 读到陈旧值,故不用它)。\n tokenUsage: { input: event.message.usage.inputTokens, output: event.message.usage.outputTokens },\n })\n }\n }\n\n private onTurnStart = (event: {\n turnIndex: number\n toolTurnCount: number\n maxToolTurns: number\n promptOutputTokens: number\n promptOutputTokenBudget?: number\n }): void => {\n this.publish({\n 'turn.start': [\n {\n type: 'turn.start' as const,\n sessionId: this.id,\n turnIndex: event.turnIndex,\n toolTurnCount: event.toolTurnCount,\n maxToolTurns: event.maxToolTurns,\n promptOutputTokens: event.promptOutputTokens,\n promptOutputTokenBudget: event.promptOutputTokenBudget,\n },\n ],\n })\n }\n\n // 状态栏占用条渐进刷新(cli interactive-events.ts `case 'turn.end'` 消费):单 prompt 内\n // 多轮工具调用循环时,每个 turn 完成即广播,避免占用条冻结到整个 prompt 结束才跳变一次。\n // 此前 AgentSession 只订阅了 turn.start(见 onTurnStart),从未转发 agent 内部已 emit 的\n // turn.end(work-loop.ts `emitter.emit('turn.end', ...)`),协议层类型(session-event.ts\n // `turn.end`)与 cli 消费端 switch-case 均已就绪但缺这一环——补上转发。\n private onTurnEnd = (event: { turnIndex: number; message: AssistantMessage }): void => {\n this.publish({\n 'turn.end': [{ type: 'turn.end' as const, sessionId: this.id, turnIndex: event.turnIndex }],\n })\n }\n\n private onToolCallStart = (event: { toolCall: ToolCallEvent }): void => {\n this.publish({\n 'tool.call.start': [\n {\n type: 'tool.call.start' as const,\n sessionId: this.id,\n toolCall: {\n id: event.toolCall.id,\n name: event.toolCall.name,\n arguments: event.toolCall.arguments,\n },\n },\n ],\n })\n }\n\n private onToolCallEnd = (event: {\n toolCall: ToolCallEvent\n result: ToolResult\n contextChars?: number\n }): void => {\n this.syncTodoListFromToolResult(event)\n const fullResultText = stripAnsi(extractToolResultText(event.result))\n // 入模口径优先(clamp/spill 之后):宿主状态栏用它做上下文实时估算。引擎未提供时\n // (旧事件源/测试桩)回退原始文本长度——宁可高估也不低估上下文压力。\n const contextChars = event.contextChars ?? fullResultText.length\n this.publish({\n 'tool.call.end': [\n {\n type: 'tool.call.end' as const,\n sessionId: this.id,\n toolCall: {\n id: event.toolCall.id,\n name: event.toolCall.name,\n arguments: event.toolCall.arguments,\n },\n isError: event.result.isError === true,\n // 残影修复(2026-07):resultText 按本次 toolCall 真实结果构造,不再靠消费端从\n // session.messages() 倒查\"最后一条 tool_result\"猜测(并行/异步工具场景下该猜测\n // 100% 错配)。截断长度对齐 trace 预览既有惯例(ENGINE_DEFAULTS.traceResultMaxChars),\n // 完整结果仍在 tool_result 消息里,事件面只需 UI 预览量级。\n resultText: truncateResultText(fullResultText, ENGINE_DEFAULTS.traceResultMaxChars),\n resultTokensEstimate: Math.ceil(contextChars / 4),\n // 修复(2026-07 P0-B1):携带工具结果的 details 字段供 a2ui 渲染器消费。\n // ToolResult.details 是插件放置任意结构化数据的地方(如 deploy-status 的\n // 部署状态对象),此前事件只携带 resultText 文本预览,a2ui.renderer toPayload\n // 函数读不到 m?.details,跨端渲染完全失效。现在作为事件字段原样透传。\n details: event.result.details ?? undefined,\n },\n ],\n })\n }\n\n /**\n * RFC-094 D2:todoList 会话持久化单源基线——write_todos 成功后把权威快照\n * (result.details.todos,set/update 均为合并后完整列表)写进 session metadata。\n * 修活非 CLI 前端(service/ACP)的 todo-reminder:其数据源 = metadata().config.todoList,\n * 此前全仓只有 CLI 投影层回写,service 模式下 reminder 永不注入。\n * 保序:publish 之前同步写 baseline;CLI 订阅回调随后覆写 turn-enriched 版(携回合号)。\n * 既有条目的 turn 按 id 保留,runtime 不知道回合投影、不清 CLI 写入的 turn。\n */\n private syncTodoListFromToolResult(event: { toolCall: ToolCallEvent; result: ToolResult }): void {\n if (event.toolCall.name !== 'write_todos' || event.result.isError) return\n const todos = event.result.details?.todos\n if (!Array.isArray(todos)) return\n const previous = new Map(\n (this.session.getTodoList() ?? []).map((t) => [t.id, t.turn] as const),\n )\n this.session.setTodoList(\n (todos as Array<{ id?: string; title?: string; status?: string }>).map((t) => ({\n id: t.id,\n title: t.title,\n status: t.status as 'pending' | 'in_progress' | 'done' | 'skipped' | 'failed' | undefined,\n turn: t.id !== undefined ? previous.get(t.id) : undefined,\n })),\n )\n }\n\n /** 本次 prompt 是否触发了预算收尾 steering(供 runPrintMode 在优雅路径发提示)。 */\n public budgetSteeredThisPrompt = false\n\n /**\n * 归档当前会话:将会话状态置为 archived,禁止后续输入。\n * 仅 idle/completed/error/aborted 等终态可归档。\n * 同步写 `metadata.archived` 持久化镜像(运行时权威仍是 Agent 状态机),\n * 供跨重启/驱逐 restore 后回放归档态(见 `PersistenceSync.restore`)。\n */\n archive(): void {\n this.ensureNotDisposed()\n this.agent.archive()\n this.session.setArchived(true)\n }\n\n /**\n * 取消归档:将会话从 archived 恢复为 idle。\n */\n unarchive(): void {\n this.ensureNotDisposed()\n this.agent.unarchive()\n this.session.setArchived(false)\n }\n\n /**\n * 置顶会话(/resume 列表排序权重)。与归档状态正交,不影响是否可输入。\n * `group`(RFC-158):置顶分组名,空白字符串规整为默认组(`undefined`)。\n */\n pin(group?: string): void {\n this.ensureNotDisposed()\n this.session.setPinned(true, group?.trim() || undefined)\n }\n\n /** 取消置顶。无条件清空分组(不保留旧分组,见 RFC-158 §4)。 */\n unpin(): void {\n this.ensureNotDisposed()\n this.session.setPinned(false)\n }\n\n async prompt(text: string, options?: PromptOptions): Promise<void> {\n this.ensureNotDisposed()\n\n if (this.status === 'archived') {\n throw new Error('Session is archived. Unarchive it first before sending messages.')\n }\n\n if (this.isBusy) {\n const { streamingBehavior } = options ?? {}\n if (streamingBehavior === 'followUp') {\n this.followUp(text)\n } else if (streamingBehavior === 'steer') {\n this.steer(text)\n } else {\n throw new Error(\n 'Agent is processing. Specify streamingBehavior (\"steer\" or \"followUp\") to queue the message.',\n )\n }\n return\n }\n\n // RFC-194 D1:本次 prompt 生命周期 Promise 快照——clearContext() 的 settle 依赖它等\n // in-flight 管线(含 todo 闸门续跑、catch 路径 persistNew)整体收尾。赋值必须在上方\n // isBusy 守卫之后(busy 路径 steer/followUp early-return 绝不覆盖 in-flight 的\n // Promise);仅顶层 prompt() 设置(runPromptRound 续跑轮天然被同一 Promise 覆盖)。\n const run = this.executePrompt(text, options)\n this.currentPrompt = run\n try {\n await run\n } finally {\n if (this.currentPrompt === run) this.currentPrompt = undefined\n }\n }\n\n /** prompt() 的执行体(RFC-194 D1 拆出——prompt() 需在 isBusy 守卫后拿到本次执行的 Promise)。 */\n private async executePrompt(text: string, options?: PromptOptions): Promise<void> {\n this.promptGate = true\n this.budgetSteeredThisPrompt = false\n this.runStartedThisPrompt = false\n this.abortRequested = false\n // RFC-180 D4:防御性复位——防止上一轮暂停后异常路径未清空、污染本次新生命周期\n // (正常路径下 resumeRun()/abort() 已经清过,这里是双重保险,同 abortRequested)。\n this.pauseRequested = false\n // 本次 prompt 真正执行所用的模型快照——回合过程中 this.model 可能被 /model 等操作改写\n // (applyModel 无条件写入,见 interactive-model.ts),prompt.end 事件须携带\"这一轮实际用的模型\",\n // 而非投影层在事件消费时刻惰性读取的活跃 session.model(见 project.ts ctx.currentModel)。\n const modelUsedThisPrompt = this.model.id\n // RFC-235:同 model 快照捕获本轮实际 effort 档(this.thinkingLevel),供回合摘要行显示 ·Effort。\n const effortUsedThisPrompt = this.thinkingLevel\n\n try {\n this.publish({\n 'prompt.start': [{ type: 'prompt.start' as const, sessionId: this.id, text }],\n })\n\n // RFC-078 M133-01:首条用户消息同步物化 derived 占位标题(零延迟、立即落库、三表面可读)。\n // 仅当尚无标题时设置;setTitle 等级守卫保证不会覆盖更晚到的 ai/custom(且 derived 取首条消息)。\n if (!this.session.metadata().title?.trim()) {\n const derived = deriveSessionTitle([{ role: 'user', content: text } as AgentMessage])\n if (derived) this.session.setTitle(derived, 'derived')\n }\n\n this.logger.info({ sessionId: this.id }, 'Session prompt started')\n\n const first = await this.runPromptRound(text, undefined, options?.images)\n if (first.cancelled) {\n this.publish({ 'prompt.end': [{ type: 'prompt.end' as const, sessionId: this.id, tokenUsage: this.lastTokenUsage, model: modelUsedThisPrompt, thinkingLevel: effortUsedThisPrompt, stopReason: 'interrupted' as const }] })\n return\n }\n\n // RFC-094 D1/D4:todo 停机闭环硬闸——回合结束但 todoList 仍有未完成项时自动续跑。\n // 每轮续跑重走完整 prompt 管线 → 新 buildContext → todo-reminder systemTail 随行\n // (RFC-042 D1 耦合闭环)。事件语义(D3):单对 prompt.start/end,中间发 prompt.continued。\n await this.todoGateRunner.run()\n\n // RFC-078 M134-03:回合完成后 fire-and-forget 生成 ai 标题(不阻塞回合,R4)。\n this.titleManager.maybeGenerate()\n\n this.publish({ 'prompt.end': [{ type: 'prompt.end' as const, sessionId: this.id, tokenUsage: this.lastTokenUsage, model: modelUsedThisPrompt, thinkingLevel: effortUsedThisPrompt, stopReason: 'completed' as const }] })\n this.logger.info({ sessionId: this.id }, 'Session prompt finished')\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n this.publish({ 'prompt.end': [{ type: 'prompt.end' as const, sessionId: this.id, tokenUsage: this.lastTokenUsage, model: modelUsedThisPrompt, thinkingLevel: effortUsedThisPrompt, stopReason: 'error' as const }] })\n this.publish({ error: [{ type: 'error' as const, sessionId: this.id, error: err }] })\n this.logger.error({ sessionId: this.id, err: err.message }, 'Session prompt failed')\n throw error\n } finally {\n this.promptGate = false\n }\n }\n\n /** 跑一轮完整 prompt 管线(buildContext → workLoop → persist),回填会话级参数。\n * hadToolActivity:本轮是否有真实工具调用产出(assistant tool_call 消息),供闸门\n * 无进展判定区分\"零动作卡死\"(放行)与\"有产出但没同步 todo 状态\"(定向续跑一次)。\n * images(RFC-111 D4b):仅首轮(外层 `prompt()` 调用)传入,todo 续跑等内部触发轮\n * 不重复携带图片——避免同一张图片在续跑轮里被重复注入上下文。 */\n private async runPromptRound(\n text: string,\n internal?: boolean,\n images?: PromptOptions['images'],\n ): Promise<{ cancelled: boolean; hadToolActivity: boolean }> {\n // RFC-324 D1 M2:压缩前置准入门——逃生阀压缩 pending 时,先 await 完成再发 prompt。\n // 解决 fire-and-forget 压缩不收敛的问题:图片密集会话(token 闸失明)真正依赖\n // 逃生阀路径,若压缩 5s 内可追加 50 条消息,压缩永远跟不上 → 滑向 lossy。\n // gate 保证压缩完成后才发新 prompt——压缩期间消息不追加,闭环收敛。\n if (this.compactionPending) {\n const pending = this.compactionPending\n this.compactionPending = undefined\n try {\n // R5 超时兜底:Promise.race 降级——超时不永久阻塞 turn,降级为 fire-and-forget。\n // 降级后 compactionPending 已清,下回合不再 gate,但 lossy 计数不重置(继续累积)。\n // 超时值可经 .otto 配置的 residency.compaction_timeout_ms 调整(env 优先);\n // 惰性读——settings 在 App 构造后才 load,构造期快照会永远是默认值。\n const COMPACTION_TIMEOUT_MS =\n this.getCompactionTimeoutMs?.() ?? SESSION_COMPACTION_TIMEOUT_MS\n await Promise.race([\n pending,\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('compaction timeout')), COMPACTION_TIMEOUT_MS),\n ),\n ])\n } catch {\n // 压缩失败或超时——不阻塞 turn,让它继续(lossy 由 persistence-sync 的\n // escapeValveFailures 门控,此处不需要重复计数)。仅清 pending 防下次重复 gate。\n }\n }\n if (this.promptRefresh) {\n const refreshed = await this.promptRefresh()\n if (refreshed !== undefined) {\n this.systemPrompt = refreshed\n }\n }\n const result = await prompt({\n text,\n internal,\n images,\n sessionId: this.id,\n agentName: this.agentName,\n model: this.model,\n tools: this.model.supportsTools === false ? [] : this.tools,\n params: {\n systemPrompt: this.systemPrompt,\n systemTail: this.systemTail,\n thinkingLevel: this.thinkingLevel,\n cacheRetention: this.interactive ? 'long' : 'short',\n },\n runner: this.agent,\n hooks: this.hookRegistry,\n persistence: this.persistence,\n memory: this.memory,\n debug: this.devtools?.debugger,\n recorder: this.recorder,\n clock: this.clock,\n isPauseRequested: () => this.pauseRequested,\n describeImages: this.describeImages,\n })\n\n if (result.cancelled) return { cancelled: true, hadToolActivity: false }\n\n // RFC-186 D8:post-workLoop 检测——workLoop 已正常 return(非 abort),若\n // pauseRequested 仍为 true,说明本次 workLoop 是因暂停而退出(turn 边界安全落盘后\n // break)。此时 publish run.paused 事件,TUI 消费方据此将 PauseOverlay 从\n // phase='requested' 切换为 phase='active',状态栏从\"等待中\"切换为\"已暂停\"。\n if (this.pauseRequested) {\n this.publish({ 'run.paused': [{ type: 'run.paused' as const, sessionId: this.id }] })\n }\n\n this.systemPrompt = result.params.systemPrompt\n this.thinkingLevel = result.params.thinkingLevel\n this.promptUsage(result.newMessagesUsageSummary)\n return { cancelled: false, hadToolActivity: result.newMessagesUsageSummary.hadToolActivity }\n }\n\n /**\n * RFC-337 D2:注入一条 steer 消息并返回其稳定 `id`(供 cli 侧持有以便 ESC 撤回)。\n *\n * id 用会话内单调计数器生成(`steer-<n>`),不裸调 `randomUUID()`/`Date.now()`——steer 是\n * UI 侧信道、不在确定性重放路径上,计数器已足够在队列生命周期内唯一,且天然确定性。\n * id 写入 `Message.uuid`,`Agent.removeSteer(uuid)` 据此按序移除。\n */\n steer(text: string): { id: string } {\n this.ensureNotDisposed()\n\n const id = `steer-${++this.steerSeq}`\n const message: Message = {\n role: 'user',\n timestamp: this.clock?.now() ?? Date.now(),\n content: [{ type: 'text', text }],\n uuid: id,\n }\n\n this.agent.steer(message)\n return { id }\n }\n\n /**\n * RFC-337 D2:按 id 移除一条尚未被模型消费的 steer 消息,返回是否命中。\n * 未命中(已被 `drainSteeringQueue` 交付进 LLM 上下文)时返回 `false`——调用方据此退化\n * 为「中止整回合」(见 RFC-337 §D3)。转调 `Agent.removeSteer`,只操作内存态队列。\n */\n removeSteer(id: string): boolean {\n this.ensureNotDisposed()\n return this.agent.removeSteer(id)\n }\n\n followUp(text: string): void {\n this.ensureNotDisposed()\n\n this.agent.followUp({\n role: 'user',\n timestamp: this.clock?.now() ?? Date.now(),\n content: [{ type: 'text', text }],\n })\n }\n\n abort(): void {\n // RFC-094 D4:闸门循环轮间隙(isExecuting=false、promptGate=true)abort 会落进\n // 下方守卫的 no-op——先置 abortRequested 让闸门循环在下一轮续跑前退出。\n if (this.promptGate) this.abortRequested = true\n // RFC-180 D4(独立评审 F3,从风险描述升级为强制改动):abort 意图无论是否早退都\n // 应清空 pauseRequested——防止用户先暂停、又立即中止(或暂停从未真正生效)时,\n // 残留的 pauseRequested=true 污染下一次 prompt() 生命周期。\n this.pauseRequested = false\n const inFlight = this.isExecuting\n const inPrep = this.promptGate && !this.runStartedThisPrompt\n if (!inFlight && !inPrep) return\n this.rejectPendingGates()\n this.agent.abort()\n }\n\n /**\n * RFC-180 D4:请求在当前 turn 边界后暂停(不影响当前 in-flight turn,安全落盘后生效)。\n * 空闲(`!isBusy`)时 no-op 并返回 false——暂停语义只对\"正在跑\"的 agent 有意义;\n * 弹窗竞态场景(Esc 弹窗打开期间 agent 恰好跑完)静默忽略,与 abort() 的\n * `inFlight`/`inPrep` 双重早退判定对齐。返回值供调用方感知(独立评审 F5):\n * `false` 时 CLI 层应给出\"Agent 已完成当前回合,无需暂停\"的 toast,而非让用户\n * 以为按钮没反应。\n */\n pauseRun(): boolean {\n this.ensureNotDisposed()\n if (!this.isBusy) return false\n this.pauseRequested = true\n // RFC-186 D2:pauseRun() 在请求时刻发布 run.pause_requested(非 run.paused)——\n // run.paused 现在由 post-workLoop 检测点在 turn 边界真正生效后发布,确保消费者\n // (TUI 浮层/状态栏)能精确区分\"等待中\"与\"已暂停\"两个阶段。\n this.publish({ 'run.pause_requested': [{ type: 'run.pause_requested' as const, sessionId: this.id }] })\n return true\n }\n\n /**\n * RFC-186 D4:取消等待中的暂停请求——仅在 turn 边界尚未到达时有效(isBusy=true)。\n * 暂停已生效(workLoop 已 break,isBusy=false)时返回 false——此时只能走 resumeRun(),\n * 不能通过\"取消\"跳过已暂停态直接进入下一回合。返回值供调用方感知:false 时 CLI 层\n * 应引导用户使用 /continue(而非 /pause),或 toast 提示\"暂停已生效,请用 /continue 恢复\"。\n */\n cancelPauseRun(): boolean {\n this.ensureNotDisposed()\n if (!this.isBusy) return false\n if (!this.pauseRequested) return false\n this.pauseRequested = false\n this.publish({ 'run.pause_cancelled': [{ type: 'run.pause_cancelled' as const, sessionId: this.id }] })\n return true\n }\n\n /**\n * RFC-180 D4:解除暂停。未处于暂停态时 no-op(不重复 publish 事件)。清空标志后,\n * `isBusy` 判定的暂停附加项自动消失——TUI 侧 `busyWithPause` 判忙链路下一次读取\n * 即感知空闲,`drainQueue` 可正常出队。\n */\n resumeRun(): void {\n this.ensureNotDisposed()\n if (!this.pauseRequested) return\n this.pauseRequested = false\n this.publish({ 'run.resumed': [{ type: 'run.resumed' as const, sessionId: this.id }] })\n }\n\n /** RFC-180 D4:是否已请求(或已生效)turn 边界暂停。与 `isBusy` 正交,见字段注释。 */\n get isPaused(): boolean {\n return this.pauseRequested\n }\n\n // ── 压缩(引擎写回 / manual /compact)——实现在 agent-session-compaction.ts,此处委托 ──\n\n async compact(summary: string, compactedCount: number): Promise<void> {\n this.ensureNotDisposed()\n await compactSession(this.compactionDeps(), summary, compactedCount)\n }\n\n async compactNow(\n signal?: AbortSignal,\n ): Promise<{ compacted: boolean; before: number; after: number }> {\n this.ensureNotDisposed()\n return compactSessionNow(\n { ...this.compactionDeps(), memory: this.memory, persistence: this.persistence },\n signal,\n )\n }\n\n private compactionDeps() {\n return {\n session: this.session,\n sessionId: this.id,\n hookRegistry: this.hookRegistry,\n logger: this.logger,\n publish: (event: Parameters<typeof this.publish>[0]) => this.publish(event),\n }\n }\n\n /**\n * `/clear`——会话状态重置(非清屏)。在状态核打一道 clear 边界:\n * · buildContext()(送 LLM)此后真空——模型上下文立即释放;\n * · visibleMessages()(TUI/--continue 重绘)此后为空——界面不再重现旧内容;\n * · messages()(完整转录)保留全部——时间旅行/审计仍可回溯 clear 前。\n * 同时驱逐 L2 工作记忆桶(previousSummaries),保 L1 持久知识、不动 L3 归档。\n * 不换 sessionId、不销毁会话(契合 otto 显式 id 的 --continue)。\n * publish 'session.cleared' → persistence-sync 立即落盘(保证新进程 --continue 反映清后态)。\n */\n async clearContext(): Promise<void> {\n this.ensureNotDisposed()\n // RFC-194 D1:busy 守卫——prompt 进行中先 abort 再 settle,杜绝 in-flight 管线的\n // turn-persist(RFC-170 D4)把携带旧上下文的消息 append 到 clear 边界之后(旧消息\n // \"复活\"竞态)。settle 等整个 prompt() 生命周期(含 todo 续跑、catch 路径 persistNew);\n // abort 路径 prompt 可能 reject——吞掉,abort 引发的错误不是 clear 的失败。\n // 已知感知延迟:/clear 落在 turn 间隙(收尾期)时 abort() 走 no-op 守卫,靠\n // abortRequested 让闸门循环下一轮前退出——最多多跑 1 turn,消息落在边界之前,安全。\n if (this.isBusy) {\n this.abort()\n await this.currentPrompt?.catch(() => {})\n }\n const clearedCount = this.session.messages().length\n this.session.clearContext()\n this.memory?.clearSession?.(this.id)\n this.logger.info({ sessionId: this.id, clearedCount }, 'Session context cleared (/clear)')\n this.publish({\n 'session.cleared': [{ type: 'session.cleared' as const, sessionId: this.id, clearedCount }],\n })\n }\n\n // ── F6 (M-D): CLI 门面 —— 消 cli 直捅 session 内脏 ──\n\n /** 清除本轮临时面板状态(todo/编辑文件/子代理列表/回合摘要),保留对话历史。 */\n clearTurnPanels(): void {\n this.session.setTodoList([])\n this.session.setEditedFiles([])\n this.session.setSubagents([])\n // /clear 会重置 projState.currentTurn 为 0(见 handlers/session.ts resetCurrentTurn),\n // 回合号从此复用——旧 turnSummaries 条目的 turn 字段会与 clear 后的新回合号碰撞\n // (如 clear 前 turn=5 的摘要,clear 后新回合也从 1 开始,resume 插回时会按 turn\n // 排序错位)。turnSummaries 持久化的目的是\"resume 时重绘可见对话流\",/clear 后旧\n // 摘要对应的转录已被 clear 边界切断(visibleMessages 从边界之后取),保留旧摘要\n // 只会产生指向不存在内容的孤儿数据,清空是正确行为。\n this.session.setTurnSummaries([])\n }\n\n /** 重命名会话(custom 等级,永不被 ai/derived 覆盖)。 */\n renameSession(title: string): void {\n this.session.setTitle(title, 'custom')\n }\n\n private promptUsage(summary: NewMessagesAccumulator): void {\n const usage = summary.usage\n // 若累加器未遇到任何 assistant 消息(lastAssistantApi 未设置),跳过统计\n // ——等价于旧代码 collectUsage 返回 null 的语义。\n if (summary.count === 0 || summary.lastAssistantApi === undefined) {\n return\n }\n\n this.lastTokenUsage = { input: usage.inputTokens, output: usage.outputTokens }\n // RFC-019 M1:每回合完成即计算命中率——api 取本回合最后一条 assistant 消息的真实 api,\n // 而非会话建立时的静态 model.api(model switch 场景下两者可能不同)。\n this.lastCacheHitRatio = computeCacheHitRatio(usage, summary.lastAssistantApi)\n\n const turnCost = this.costTracker.record(this.model, usage)\n const sessionCost = this.costTracker.summary().totalCostUSD\n\n this.logger.info(\n {\n sessionId: this.id,\n input: usage.inputTokens,\n output: usage.outputTokens,\n cacheRead: usage.cacheReadTokens,\n cacheWrite: usage.cacheWriteTokens,\n cacheHitRatio: this.lastCacheHitRatio,\n turnCost: turnCost.toFixed(6),\n sessionCost: sessionCost.toFixed(6),\n },\n 'Session usage',\n )\n\n // RFC-204 D1/D2:会话级成本软预算——一次性提醒,不阻断。budget>0 校验对齐\n // RFC-104 promptOutputTokenBudget 的既有纹理(<=0 视为未配置)。无定价模型\n // (Model.cost undefined)恒 totalCostUSD===0(packages/ai/src/cost.ts),\n // 预算检测自然恒不触发——与既有 /cost 面板\"无定价不显示\"是同一限制,非本机制新引入。\n if (\n !this.sessionCrossedCostBudget &&\n this.sessionCostBudgetUSD !== undefined &&\n this.sessionCostBudgetUSD > 0 &&\n sessionCost >= this.sessionCostBudgetUSD\n ) {\n this.sessionCrossedCostBudget = true\n this.publish({\n 'session.cost-budget-exceeded': [\n {\n type: 'session.cost-budget-exceeded' as const,\n sessionId: this.id,\n totalCostUSD: sessionCost,\n budgetUSD: this.sessionCostBudgetUSD,\n },\n ],\n })\n }\n }\n\n /** H5-20:本会话累计用量 + 成本汇总(供 /cost 命令 / TUI 状态栏消费)。 */\n getCostSummary(): CostSummary {\n return this.costTracker.summary()\n }\n\n /**\n * RFC-019 M1:最近一回合的缓存命中率(R1)。`null` = 供应商无缓存 API 或本回合无 usage 数据\n * ——消费方应据此隐藏该字段,而非展示误导性的 0。供 `/cache-stats`、summary 行消费。\n */\n getLastCacheHitRatio(): number | null {\n return this.lastCacheHitRatio\n }\n\n override subscribe(callback: AgentSessionSubscriber): () => void\n override subscribe<K extends AgentSessionEventType>(\n type: K,\n callback: AgentSessionEventMap[K],\n once?: boolean,\n ): () => void\n override subscribe<K extends AgentSessionEventType>(\n subscriberOrType: AgentSessionSubscriber | K,\n callback?: AgentSessionEventMap[K],\n once?: boolean,\n ): () => void {\n if (typeof subscriberOrType === 'function') {\n return this.subscribeAll((_type: unknown, event: unknown) => {\n subscriberOrType(event as AgentSessionEvent)\n })\n }\n\n if (!callback) {\n throw new Error('callback is required when subscribing by type')\n }\n\n return super.subscribe(subscriberOrType, callback, once)\n }\n\n // ── HITL 三 gate(approval/grill/plan)——实现在 agent-session-gates.ts,此处委托保持公共 API 不变 ──\n\n async requestApproval(\n toolName: string,\n args: Record<string, unknown>,\n description: string,\n risk?: ApprovalRisk,\n ): Promise<boolean> {\n return this.gates.requestApproval(toolName, args, description, risk)\n }\n\n resolveApproval(approvalId: string, approved: boolean): void {\n this.gates.resolveApproval(approvalId, approved)\n }\n\n async requestGrill(q: Omit<GrillQuestion, 'id'>): Promise<GrillAnswer> {\n return this.gates.requestGrill(q)\n }\n\n resolveAskUser(requestId: string, answer: GrillAnswer | null): void {\n this.gates.resolveAskUser(requestId, answer)\n }\n\n async requestPlanApproval(planContent: string): Promise<{ action: string; feedback?: string }> {\n return this.gates.requestPlanApproval(planContent)\n }\n\n resolvePlanApproval(requestId: string, action: string, feedback?: string): void {\n this.gates.resolvePlanApproval(requestId, action, feedback)\n }\n\n private rejectPendingGates(): void {\n this.gates.rejectAll(new Error('Session aborted'))\n }\n\n private ensureNotDisposed(): void {\n if (this.disposed) {\n throw new Error(`AgentSession ${this.id} has been disposed`)\n }\n }\n\n /**\n * RFC-078 M135-01:显式回血——对 title 空/derived 的(已加载)会话强制重生 ai 标题,\n * 绕过「前 3 回合」成本闸(回血是一次性批处理意图)。custom 仍受守卫保护。\n */\n backfillTitle(): void {\n this.titleManager.backfill()\n }\n\n dispose(): void {\n if (this.disposed) {\n return\n }\n\n this.disposed = true\n this.titleManager.dispose() // 中止任何 in-flight 标题生成(R4)\n this.rejectPendingGates()\n\n for (const unsub of this.agentUnsubs) {\n unsub()\n }\n this.agentUnsubs.length = 0\n\n // RFC-180 D2(独立评审 P1 F2):若会话是因 turn 边界暂停正常收尾(workLoop 已\n // return,Agent.status 已是 'completed',无 in-flight 操作),跳过 agent.abort()——\n // 否则会把\"优雅暂停后完成\"的会话语义错误地强转为\"被中止\"(真实触发场景:用户暂停\n // 后长时间未操作,SessionPool idle/LRU 驱逐调用本方法)。仍有 in-flight 执行的\n // 会话(真正需要中断的场景)不受影响,照常 abort。\n if (!(this.pauseRequested && this.agent.status === 'completed')) {\n this.agent.abort()\n }\n this.memory?.forgetSession?.(this.id)\n this.publish({ 'session.end': [{ type: 'session.end' as const, sessionId: this.id }] })\n this.removeAllListeners()\n\n this.logger.info({ sessionId: this.id }, 'Session disposed')\n }\n}\n","import { InMemorySession } from '@x-otto/session'\nimport type { SessionEntry, SessionMetadata } from '@x-otto/session'\n\nimport type { AgentMessage, ClockPort } from '@x-otto/agent'\n\n/**\n * RFC-020 M53 T4:fork 子树原语——从源会话的 entries(到某点)+ leafId 建一个携带标准 fork\n * 元数据的新会话树。`forkSession`(当前 leaf 全量快照)与 `forkFromCheckpoint`(历史 checkpoint\n * 点的快照)共用此唯一原语,统一 fork 元数据约定:`parentSessionId` / `forkPoint` / `tags + ['fork']`。\n *\n * 范围:只统一\"建分叉树\"这一段。会话采纳副作用(`session.created` emit + persistence tracking\n * + notifyCreated)已由 `SessionManager.adoptForkedSession` 统一(两路 fork 共用,皆\"一等可观测 +\n * 落盘持久\");trace/checkpoint 复制是 checkpoint-fork 特有,仍由 forkFromCheckpoint 自理。\n */\nexport function buildForkedSession(\n newId: string,\n sourceId: string,\n entries: readonly SessionEntry<AgentMessage>[],\n metadata: SessionMetadata,\n leafId: string | undefined,\n clock?: ClockPort,\n): InMemorySession<AgentMessage> {\n const now = clock?.now() ?? Date.now()\n const forked = new InMemorySession<AgentMessage>(newId, sourceId, entries.length)\n forked.restoreEntries(\n [...entries],\n {\n ...metadata,\n createdAt: now,\n lastActiveAt: now,\n parentSessionId: sourceId,\n forkPoint: entries.length,\n tags: [...metadata.tags, 'fork'],\n },\n leafId,\n )\n return forked\n}\n","import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { dirname, resolve, sep } from 'node:path'\nimport type { AgentMessage } from '@x-otto/agent'\nimport type { SessionSnapshot } from '@x-otto/session'\n\nexport type RestoreMode = 'conversation' | 'code' | 'both'\n\nexport interface FileEntrySnapshot {\n path: string\n content: string | null\n}\n\nexport interface FileSnapshot {\n files: FileEntrySnapshot[]\n}\n\nexport interface CheckpointData {\n sessionId: string\n label?: string\n createdAt: number\n traceSeq: number\n conversation: SessionSnapshot<AgentMessage>\n files?: FileSnapshot\n}\n\nexport interface Checkpoint extends CheckpointData {\n seq: number\n}\n\nfunction resolveUnder(workspaceDir: string, path: string): string {\n const resolvedWorkspace = resolve(workspaceDir)\n const resolved = resolve(resolvedWorkspace, path)\n\n if (!resolved.startsWith(resolvedWorkspace + sep) && resolved !== resolvedWorkspace) {\n throw new Error(\n `路径穿越拒绝:路径 \"${path}\" 不在工作目录 \"${workspaceDir}\" 内`,\n )\n }\n\n return resolved\n}\n\nfunction isEnoent(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n (error as { code: string }).code === 'ENOENT'\n )\n}\n\nexport async function captureFiles(workspaceDir: string, paths: string[]): Promise<FileSnapshot> {\n const files: FileEntrySnapshot[] = []\n for (const path of paths) {\n const full = resolveUnder(workspaceDir, path)\n try {\n files.push({ path, content: await readFile(full, 'utf-8') })\n } catch (error) {\n if (isEnoent(error)) {\n files.push({ path, content: null })\n } else {\n throw error\n }\n }\n }\n return { files }\n}\n\nexport async function restoreFiles(workspaceDir: string, snapshot: FileSnapshot): Promise<void> {\n for (const entry of snapshot.files) {\n const full = resolveUnder(workspaceDir, entry.path)\n if (entry.content === null) {\n await rm(full, { force: true })\n } else {\n await mkdir(dirname(full), { recursive: true })\n await writeFile(full, entry.content, 'utf-8')\n }\n }\n}\n","import type { StreamEvent } from '@x-otto/ai'\nimport type { ToolResult } from '@x-otto/agent'\n\n/**\n * trace 事件解码的共享判别面(replay 与 traceback 共用,去 dup-1 重复)。\n * 二者都从 `TraceEvent.payload` 还原流式/工具结果——判别谓词与 payload 形状本是逐字重复。\n */\n\nexport function isStreamEvent(payload: unknown): payload is StreamEvent {\n return typeof payload === 'object' && payload !== null && 'type' in payload\n}\n\n/** tool.call.end trace 的 resultSummary(M28 压缩版 ToolResult)。 */\nexport interface ToolResultSummary {\n isError: boolean\n contentCount: number\n contentPreview?: Array<{ type: string; text?: string; preview?: string; totalLength?: number }>\n details?: Record<string, unknown>\n}\n\n/**\n * tool.call.end trace payload。生产者(work-loop)恒带 `id`+`name`;\n * `result`=旧全量 shape,`resultSummary`=M28 压缩 shape(二选一)。\n */\nexport interface ToolEndPayload {\n id: string\n name: string\n result?: ToolResult\n resultSummary?: ToolResultSummary\n}\n\nexport function isToolEndPayload(payload: unknown): payload is ToolEndPayload {\n return (\n typeof payload === 'object' &&\n payload !== null &&\n 'id' in payload &&\n 'name' in payload &&\n ('result' in payload || 'resultSummary' in payload)\n )\n}\n\n/** resultSummary.contentPreview → text 内容数组(截断预览的 text/preview 回退解码,二者共用)。 */\nexport function previewToContent(summary: ToolResultSummary): Array<{ type: 'text'; text: string }> {\n return summary.contentPreview?.map((c) => ({ type: 'text' as const, text: c.text ?? c.preview ?? '' })) ?? []\n}\n","import type { AssistantMessage, Message, ToolResultMessage } from '@x-otto/ai'\nimport type { TraceEvent } from '@x-otto/agent'\nimport type { SessionEntry, SessionSnapshot } from '@x-otto/session'\nimport { isStreamEvent, isToolEndPayload, previewToContent, type ToolEndPayload } from './trace-decode'\n\nexport interface TracebackView {\n toSeq: number\n baselineCheckpointSeq: number | null\n fromTraceSeq: number\n events: TraceEvent[]\n messages: Message[]\n}\n\nexport function messagesFromSnapshot(snapshot: SessionSnapshot): Message[] {\n const byId = new Map<string, SessionEntry>()\n for (const entry of snapshot.entries) {\n byId.set(entry.id, entry)\n }\n\n const branch: SessionEntry[] = []\n let current = snapshot.leafId ? byId.get(snapshot.leafId) : undefined\n while (current) {\n branch.push(current)\n current = current.parentId ? byId.get(current.parentId) : undefined\n }\n branch.reverse()\n\n return branch\n .filter((e): e is Extract<SessionEntry, { type: 'message' }> => e.type === 'message')\n .map((e) => e.message as Message)\n}\n\nfunction extractResultContent(payload: ToolEndPayload): ToolResultMessage['content'] {\n if (payload.result) {\n return payload.result.content\n }\n return payload.resultSummary ? previewToContent(payload.resultSummary) : []\n}\n\nfunction promptText(payload: unknown): string | undefined {\n if (typeof payload === 'object' && payload !== null && 'text' in payload) {\n const text = (payload as { text?: unknown }).text\n return typeof text === 'string' ? text : undefined\n }\n return undefined\n}\n\nexport function reconstructMessages(baseline: Message[], events: Iterable<TraceEvent>): Message[] {\n const messages: Message[] = [...baseline]\n\n for (const event of events) {\n if (event.kind === 'lifecycle' && event.node === 'prompt.before') {\n const text = promptText(event.payload)\n if (text !== undefined) {\n messages.push({ role: 'user', content: [{ type: 'text', text }], timestamp: event.ts })\n }\n } else if (event.kind === 'stream' && event.node === 'stream.event') {\n if (isStreamEvent(event.payload) && event.payload.type === 'done') {\n messages.push(event.payload.message as AssistantMessage)\n }\n } else if (event.kind === 'tool' && event.node === 'tool.call.end') {\n if (isToolEndPayload(event.payload)) {\n const { id, name, result, resultSummary } = event.payload\n const isError = result?.isError ?? resultSummary?.isError ?? false\n const details = result?.details ?? resultSummary?.details ?? {}\n messages.push({\n role: 'tool_result',\n toolCallId: id,\n toolName: name,\n content: extractResultContent(event.payload),\n isError,\n details,\n timestamp: event.ts,\n } satisfies ToolResultMessage)\n }\n }\n }\n\n return messages\n}\n","import { createEventStream } from '@x-otto/ai'\nimport type { AssistantMessage, StreamEvent } from '@x-otto/ai'\nimport type { AgentTool, StreamFunction, ToolResult, TraceEvent } from '@x-otto/agent'\nimport {\n isStreamEvent,\n isToolEndPayload,\n previewToContent,\n type ToolEndPayload,\n} from './trace-decode'\n\nfunction extractResult(payload: ToolEndPayload): ToolResult {\n if (payload.result) return payload.result\n if (payload.resultSummary) {\n return {\n content: previewToContent(payload.resultSummary),\n isError: payload.resultSummary.isError,\n details: payload.resultSummary.details,\n }\n }\n return {\n content: [],\n isError: true,\n details: { error: 'unable to extract result from trace payload' },\n }\n}\n\nfunction streamSegments(events: Iterable<TraceEvent>): StreamEvent[][] {\n const segments: StreamEvent[][] = []\n let current: StreamEvent[] = []\n for (const event of events) {\n if (event.kind === 'stream' && event.node === 'stream.event' && isStreamEvent(event.payload)) {\n current.push(event.payload)\n if (event.payload.type === 'done') {\n segments.push(current)\n current = []\n }\n }\n }\n if (current.length > 0) {\n segments.push(current)\n }\n return segments\n}\n\nexport function buildReplayStream(events: Iterable<TraceEvent>): StreamFunction {\n const segments = streamSegments(events)\n let cursor = 0\n\n return () => {\n const recorded = segments[cursor] ?? []\n cursor++\n\n const stream = createEventStream<StreamEvent, AssistantMessage>()\n let message: AssistantMessage | undefined\n for (const event of recorded) {\n stream.push(event)\n if (event.type === 'done') {\n message = event.message\n } else if ('partial' in event) {\n message = event.partial\n }\n }\n if (message) {\n stream.complete(message)\n } else {\n stream.fail(new Error('replay stream: no recorded LLM events for this turn'))\n }\n return stream\n }\n}\n\nexport function buildToolResults(events: Iterable<TraceEvent>): Map<string, ToolResult> {\n const results = new Map<string, ToolResult>()\n for (const event of events) {\n if (\n event.kind === 'tool' &&\n event.node === 'tool.call.end' &&\n isToolEndPayload(event.payload)\n ) {\n results.set(event.payload.id, extractResult(event.payload))\n }\n }\n return results\n}\n\nexport function wrapToolsForReplay(\n tools: AgentTool[],\n results: Map<string, ToolResult>,\n): AgentTool[] {\n return tools.map((tool) => ({\n ...tool,\n execute: async (context: { id: string }): Promise<ToolResult> => {\n const recorded = results.get(context.id)\n if (recorded) {\n return recorded\n }\n return {\n content: [{ type: 'text', text: `replay: no recorded result for tool call ${context.id}` }],\n isError: true,\n }\n },\n })) as AgentTool[]\n}\n","import type { ToolResult } from '@x-otto/agent'\n\nexport type ReplayLevel = 'L1' | 'L2'\n\nexport interface SandboxToolCall {\n toolCallId: string\n name: string\n arguments: Record<string, unknown>\n}\n\nexport interface SandboxToolResult {\n toolCallId: string\n result: ToolResult\n}\n\n/**\n * 可插拔沙箱执行器。x-web-container 将提供实现(VFS / worker / syscall / capability 隔离)。\n * L2 重执行经此接口在隔离环境真实重跑工具调用,保证宿主工作区不被重放副作用污染。\n */\nexport interface SandboxRunner {\n /** 在隔离沙箱内执行一次工具调用,返回真实结果(含副作用,但作用域限于沙箱)。 */\n execute(call: SandboxToolCall): Promise<SandboxToolResult>\n /** 释放沙箱资源(VFS / worker 等)。 */\n dispose(): Promise<void>\n}\n\n/**\n * 请求 L2 但未注入 SandboxRunner 时抛出。明确\"接口缝已留、实现待 x-web-container\",避免静默降级到 L1\n * 而让调用方误以为副作用被真实重跑。\n */\nexport class SandboxUnavailableError extends Error {\n constructor() {\n super(\n 'L2 deterministic re-execution requires a SandboxRunner (x-web-container runtime), ' +\n 'which is not yet available. Use replay level \"L1\" (record/replay of tool results) for now.',\n )\n this.name = 'SandboxUnavailableError'\n }\n}\n","import { InMemorySession, hasStrippedReplacements } from '@x-otto/session'\nimport { buildForkedSession } from './fork'\nimport { SESSION_SNAPSHOT_VERSION } from '@x-otto/session'\n\nimport type { SessionEntry } from '@x-otto/session'\nimport { createReplayPorts, extractNondetDraws } from '@x-otto/agent'\n\nimport { captureFiles, restoreFiles } from './checkpoint'\nimport { messagesFromSnapshot, reconstructMessages } from './traceback'\nimport { buildReplayStream, buildToolResults, wrapToolsForReplay } from './replay'\nimport { SandboxUnavailableError } from './sandbox'\n\nimport type { AgentMessage, AgentTool, TraceEvent } from '@x-otto/agent'\nimport type { AppendLog } from '@x-otto/persistence'\nimport type { Logger } from '@x-otto/shared'\nimport type { AgentSession } from './agent-session'\nimport type { Checkpoint, CheckpointData, RestoreMode } from './checkpoint'\nimport type { ReplayLevel, SandboxRunner } from './sandbox'\nimport type { TracebackView } from './traceback'\nimport type { ResolvedSessionConfig } from './types'\nimport type { SessionPorts } from './session-manager'\n\nexport interface TimeTravelDeps {\n logger: Logger\n traceStore?: AppendLog<TraceEvent>\n checkpointStore?: AppendLog<CheckpointData>\n getSession: (id: string) => AgentSession | undefined\n getConfig: (id: string) => ResolvedSessionConfig | undefined\n /** replaySession 走 Manager.createSession 正门(容量/登记/hook/active 维护)。 */\n createSession: (\n config: ResolvedSessionConfig & { id?: string },\n ports?: SessionPorts,\n ) => Promise<AgentSession>\n /** fork 容量预检(同名冲突 + 容量阈值驱逐),在复制 trace/checkpoint 流之前执行。 */\n ensureCapacity: (id: string) => void\n /**\n * fork 池登记(两路 fork 共用):构建 AgentSession + 登记 + 持久化追踪 + session.created hook\n * + notifyCreated(一等可观测 + 落盘持久;不改 active——与历史 fork 行为一致)。\n */\n adoptForkedSession: (\n raw: InMemorySession<AgentMessage>,\n config: ResolvedSessionConfig,\n ) => Promise<AgentSession>\n /**\n * RFC-160 D3:剥离态 compaction 的 replacement 真身回填(持久层能力,缺省 no-op 直通)。\n * checkpoint 捕获前必须调用(剥离态固化进冷存储 = 永久丢 replacement);restore 后\n * 必须调用(旧 compaction 复位为末位边界,进 buildContext 的必须真身,RFC-142)。\n */\n hydrateEntries?: (\n sessionId: string,\n entries: readonly SessionEntry<AgentMessage>[],\n ) => Promise<SessionEntry<AgentMessage>[]>\n}\n\nexport class TimeTravelController {\n constructor(private readonly deps: TimeTravelDeps) {}\n\n private replayPorts(events: Iterable<TraceEvent>, tools: AgentTool[]): SessionPorts {\n const eventList = [...events]\n const clockPorts = createReplayPorts(extractNondetDraws(eventList))\n return {\n clock: clockPorts.clock,\n stream: buildReplayStream(eventList),\n tools: wrapToolsForReplay(tools, buildToolResults(eventList)),\n disableMemory: true,\n }\n }\n\n /**\n * 确定性重放(L1)——以录制会话的 trace 构造新会话,引擎的非确定性输入全部回灌:\n * clock/random/id(端口)+ LLM 流 + 工具结果(不真实调用 provider/工具)。\n * 在该会话上重跑同一提示,产出与录制逐字节一致(含时间戳/assistant 内容/工具结果)。\n * 复用源会话已解析的配置 + 读源会话 trace。\n *\n * **重现层级**:缺省 level='L1'(回灌录制工具结果,不真实执行副作用)。level='L2'\n * 为全确定性沙箱重执行(沙箱内真实重跑工具),需注入 SandboxRunner(x-web-container 提供);未注入\n * 即抛 SandboxUnavailableError(不静默降级到 L1)。L2 的实际执行接线待 x-web-container,见 sandbox.ts。\n *\n * @param sourceSessionId 已录制(且仍在 configs 中)的源会话 id\n * @param newId 重放会话 id(缺省自动生成)\n * @param opts 重现层级与(L2 时)沙箱 runner\n */\n async replaySession(\n sourceSessionId: string,\n newId?: string,\n opts: { level?: ReplayLevel; sandbox?: SandboxRunner } = {},\n ): Promise<AgentSession> {\n if (!this.deps.traceStore) {\n throw new Error('replaySession requires a traceStore (record mode must be enabled)')\n }\n if ((opts.level ?? 'L1') === 'L2' && !opts.sandbox) {\n throw new SandboxUnavailableError()\n }\n const config = this.deps.getConfig(sourceSessionId)\n if (!config) {\n throw new Error(`Cannot replay unknown session \"${sourceSessionId}\" (no resolved config)`)\n }\n\n const events: TraceEvent[] = []\n for await (const record of this.deps.traceStore.read(sourceSessionId)) {\n events.push(record.entry)\n }\n\n return this.deps.createSession({ ...config, id: newId }, this.replayPorts(events, config.tools))\n }\n\n /**\n * 建立 checkpoint —— 会话快照(必)+ 被追踪文件的内容快照(opts.files 提供时)。\n * 是回溯/回滚的锚点。文件集由调用方给出(变更文件),属增量快照。\n */\n async createCheckpoint(\n sessionId: string,\n opts: { label?: string; files?: string[] } = {},\n ): Promise<Checkpoint> {\n if (!this.deps.checkpointStore) {\n throw new Error('createCheckpoint requires a checkpointStore')\n }\n const agentSession = this.deps.getSession(sessionId)\n if (!agentSession) {\n throw new Error(`Cannot checkpoint unknown session \"${sessionId}\"`)\n }\n const rawSession = agentSession.session\n if (!(rawSession instanceof InMemorySession)) {\n throw new Error(`Session \"${sessionId}\" does not support snapshots`)\n }\n\n if (agentSession.isBusy) {\n throw new Error(\n `Cannot checkpoint session \"${sessionId}\" while a prompt is in flight (session busy); ` +\n `checkpoint requires a quiescent session for a consistent {conversation, traceSeq}`,\n )\n }\n\n const snapshot = rawSession.toSnapshot()\n // RFC-160 D3/D4:checkpoint 是自包含冷存储(可导出/跨机)——剥离态固化进去 = replacement\n // 永久丢失。捕获前 hydrate 真身;能力缺席却存在剥离态 → 断言炸(D4 硬闸门,绝不静默)。\n const entries = await this.ensureHydrated(sessionId, snapshot.entries, 'checkpoint capture')\n const traceSeq = await this.countTraceEvents(sessionId)\n const data: CheckpointData = {\n sessionId,\n label: opts.label,\n createdAt: Date.now(),\n traceSeq,\n conversation: {\n version: SESSION_SNAPSHOT_VERSION,\n id: sessionId,\n entries,\n metadata: snapshot.metadata,\n leafId: snapshot.leafId,\n },\n files: opts.files ? await captureFiles(agentSession.workspaceDir, opts.files) : undefined,\n }\n\n const record = await this.deps.checkpointStore.append(sessionId, data)\n return { ...record.entry, seq: record.seq }\n }\n\n async listCheckpoints(sessionId: string): Promise<Checkpoint[]> {\n if (!this.deps.checkpointStore) {\n return []\n }\n const checkpoints: Checkpoint[] = []\n for await (const record of this.deps.checkpointStore.read(sessionId)) {\n checkpoints.push({ ...record.entry, seq: record.seq })\n }\n return checkpoints\n }\n\n /**\n * 恢复到某 checkpoint,三模式隔离——\n * conversation:仅还原会话消息(restoreEntries 截断/替换);code:仅写回文件快照;both:两者。\n * 各模式只动该恢复的部分(gate:模式隔离)。回滚/分叉在此之上。\n */\n async restoreCheckpoint(\n sessionId: string,\n seq: number,\n mode: RestoreMode = 'both',\n ): Promise<void> {\n if (!this.deps.checkpointStore) {\n throw new Error('restoreCheckpoint requires a checkpointStore')\n }\n const agentSession = this.deps.getSession(sessionId)\n if (!agentSession) {\n throw new Error(`Cannot restore unknown session \"${sessionId}\"`)\n }\n if (agentSession.isBusy) {\n throw new Error(\n `Cannot restore/rollback session \"${sessionId}\" while a prompt is in flight (session busy); ` +\n `abort the active prompt before rewinding`,\n )\n }\n\n const data = await this.readCheckpoint(sessionId, seq)\n\n if (mode === 'conversation' || mode === 'both') {\n const rawSession = agentSession.session\n if (rawSession instanceof InMemorySession) {\n // RFC-160 D3:restore 使旧 compaction 复位为末位边界——进 buildContext 的必须真身\n // (RFC-142 红线)。checkpoint 数据经捕获前 hydrate 本应无剥离态,此处防御旧数据/\n // 外部导入的 checkpoint。\n const entries = await this.ensureHydrated(\n sessionId,\n data.conversation.entries,\n 'checkpoint restore',\n )\n rawSession.restoreEntries(entries, data.conversation.metadata, data.conversation.leafId)\n }\n }\n\n if ((mode === 'code' || mode === 'both') && data.files) {\n await restoreFiles(agentSession.workspaceDir, data.files)\n }\n }\n\n /**\n * RFC-160 D3/D4:出口 hydrate 统一入口。无剥离态 → 原样直通(快路径);有剥离态且\n * hydrateEntries 能力在场 → 回填真身;有剥离态但能力缺席 → 断言炸(D4 硬闸门:\n * 接线遗漏必须立即暴露,绝不静默把剥离态放行到冷存储/模型上下文)。\n */\n private async ensureHydrated(\n sessionId: string,\n entries: readonly SessionEntry<AgentMessage>[],\n exit: string,\n ): Promise<SessionEntry<AgentMessage>[]> {\n if (!hasStrippedReplacements(entries)) return [...entries]\n if (!this.deps.hydrateEntries) {\n throw new Error(\n `${exit}: entries contain stripped compaction replacements but no hydrateEntries ` +\n 'capability is wired (RFC-160 D4); refusing to proceed with stripped state',\n )\n }\n return this.deps.hydrateEntries(sessionId, entries)\n }\n\n private async readCheckpoint(sessionId: string, seq: number): Promise<CheckpointData> {\n if (!this.deps.checkpointStore) {\n throw new Error('checkpoint operations require a checkpointStore')\n }\n let data: CheckpointData | undefined\n for await (const record of this.deps.checkpointStore.read(sessionId, {\n from: seq,\n to: seq + 1,\n })) {\n data = record.entry\n }\n if (!data) {\n throw new Error(`Checkpoint seq=${seq} not found for session \"${sessionId}\"`)\n }\n if (data.conversation.version !== SESSION_SNAPSHOT_VERSION) {\n throw new Error(\n `Checkpoint seq=${seq} for session \"${sessionId}\" has incompatible snapshot version ` +\n `${data.conversation.version} (expected ${SESSION_SNAPSHOT_VERSION})`,\n )\n }\n return data\n }\n\n /**\n * **原地回滚(destructive rollback)**——在 restore 三模式(会话/代码)之上,再把 trace\n * **截断**到该 checkpoint 的 traceSeq(丢弃其后的事件)。这是\n * \"trace 级截断/分叉\":回滚后 traceback/replay 不再看到被回滚分支,且 live 会话续跑的 trace\n * 从 traceSeq 续号(recorder 经 store.append 自动续号,无需 rebind)。\n *\n * 与 restoreCheckpoint 的区别:restoreCheckpoint 只动状态、不动 trace(只读历史仍可见旧分支);\n * rollbackToCheckpoint 额外截断 trace(旧分支被丢弃)——\"原地回退\"语义。\n */\n async rollbackToCheckpoint(\n sessionId: string,\n seq: number,\n mode: RestoreMode = 'both',\n ): Promise<void> {\n const data = await this.readCheckpoint(sessionId, seq)\n await this.restoreCheckpoint(sessionId, seq, mode)\n if (this.deps.traceStore) {\n await this.deps.traceStore.truncate(sessionId, data.traceSeq)\n }\n this.deps.logger.info({ sessionId, seq, traceSeq: data.traceSeq, mode }, 'Session rolled back')\n }\n\n /**\n * **从 checkpoint 分叉(fork)**——以源会话某 checkpoint 为起点开出一个新分支会话,源会话\n * 保持不变(会话分支 / fork-from-checkpoint 语义)。新会话:\n * - 会话消息 = checkpoint 的会话快照(分支点之前的历史);\n * - trace = 复制源会话 [0, traceSeq) 的事件到新 stream(使分叉会话的 traceback/replay 自足、独立);\n * - checkpoint = 复制源会话 traceSeq ≤ 分支点 的 checkpoint 到新 stream(分叉会话保有基线锚点);\n * - 工作区/代码 = 共享源会话 workspaceDir(与既有 forkSession 一致;隔离工作区属 L2 沙箱范畴)。\n * 新会话经 session.created hook 成为一等会话(service 据此挂 trace/event 桥,可观测)。\n */\n async forkFromCheckpoint(sessionId: string, seq: number, newId?: string): Promise<AgentSession> {\n const sourceSession = this.deps.getSession(sessionId)\n const sourceConfig = this.deps.getConfig(sessionId)\n if (!sourceSession || !sourceConfig) {\n throw new Error(`Cannot fork unknown session \"${sessionId}\"`)\n }\n const data = await this.readCheckpoint(sessionId, seq)\n\n const id = newId ?? crypto.randomUUID()\n this.deps.ensureCapacity(id)\n\n if (this.deps.traceStore) {\n for await (const record of this.deps.traceStore.read(sessionId, { to: data.traceSeq })) {\n await this.deps.traceStore.append(id, record.entry)\n }\n }\n if (this.deps.checkpointStore) {\n for await (const record of this.deps.checkpointStore.read(sessionId)) {\n if (record.entry.traceSeq <= data.traceSeq) {\n await this.deps.checkpointStore.append(id, { ...record.entry, sessionId: id })\n }\n }\n }\n\n // RFC-160 D3/D4:第四出口(M2 完成审核发现的缺口)——外部导入/pre-M2 的旧 checkpoint\n // 可能含剥离态,经此路径直接落**新 session_id** 的行。与 forkSession 同纪律:fork 前\n // hydrate,能力缺席 + 剥离态 → fail-fast(不依赖 appendEntry 首行断言的 lazy 后手)。\n const entries = await this.ensureHydrated(\n sessionId,\n data.conversation.entries,\n 'forkFromCheckpoint',\n )\n const forkedRaw = buildForkedSession(\n id,\n sessionId,\n entries,\n data.conversation.metadata,\n data.conversation.leafId,\n )\n\n const agentSession = await this.deps.adoptForkedSession(forkedRaw, { ...sourceConfig })\n\n this.deps.logger.info(\n { sessionId: id, sourceId: sessionId, fromSeq: seq },\n 'Session forked from checkpoint',\n )\n return agentSession\n }\n\n private async countTraceEvents(sessionId: string): Promise<number> {\n if (!this.deps.traceStore) {\n return 0\n }\n // 后端支持 count()(字节数行/缓存 O(1))时直取——巨型 trace 下避免全量 decode 只为拿总数。\n const fast = await this.deps.traceStore.count?.(sessionId)\n if (fast !== undefined) {\n return fast\n }\n let count = 0\n for await (const _record of this.deps.traceStore.read(sessionId)) {\n count++\n }\n return count\n }\n\n /**\n * 重放 trace 到某 seq(含)——返回 [from, toSeq] 区间的 trace 事件(design TimeTravel.replay)。\n * toSeq 缺省=全量。是回溯的底层读法(不改动会话,纯读 trace)。\n */\n async replayTrace(sessionId: string, toSeq?: number, fromSeq = 0): Promise<TraceEvent[]> {\n if (!this.deps.traceStore) {\n return []\n }\n const range = { from: fromSeq, to: toSeq === undefined ? undefined : toSeq + 1 }\n const events: TraceEvent[] = []\n for await (const record of this.deps.traceStore.read(sessionId, range)) {\n events.push(record.entry)\n }\n return events\n }\n\n /**\n * 回溯——重建某 trace seq 时刻的历史会话状态(只读,不改动 live 会话)。\n * 选 traceSeq ≤ toSeq 的最近 checkpoint 作基线,把其后到 toSeq 的 trace 事件折叠回消息列表\n * (user←prompt.before.text、assistant←stream done、tool_result←tool.call.end)。无 checkpoint 则从 seq 0 折叠。\n */\n async traceback(sessionId: string, toSeq: number): Promise<TracebackView> {\n const checkpoints = await this.listCheckpoints(sessionId)\n const baseline = checkpoints\n .filter((c) => c.traceSeq <= toSeq)\n .reduce<Checkpoint | undefined>(\n (best, c) => (best === undefined || c.traceSeq > best.traceSeq ? c : best),\n undefined,\n )\n\n const fromTraceSeq = baseline?.traceSeq ?? 0\n const baselineMessages = baseline ? messagesFromSnapshot(baseline.conversation) : []\n const events = await this.replayTrace(sessionId, toSeq, fromTraceSeq)\n\n return {\n toSeq,\n baselineCheckpointSeq: baseline?.seq ?? null,\n fromTraceSeq,\n events,\n messages: reconstructMessages(baselineMessages, events),\n }\n }\n}\n","/**\n * residency-governor.ts — RFC-178 触发器 C:内存压力驱动的会话驻留收紧。\n *\n * 把 MemoryGovernor 的 warning/critical 分级信号翻译成**驻留维度**的动态收紧系数:\n * - healthy → 系数 1(cap 用全值:条数 2000 / 字节 256MB)\n * - warning → 系数 1/2(条数 1000 / 字节 128MB)\n * - critical → 系数 1/4(条数 500 / 字节 64MB)\n *\n * 设计约束(RFC-178 D1/D4):\n * - **离散阶梯**而非连续值——内存采样受 GC 时机影响非确定,阶梯化把非确定性收敛到\n * \"处于哪一档\";同一档位下裁剪结果完全确定(执行器不变式)。\n * - 本类不做采样、不订阅 MemoryGovernor——纯状态容器 + 系数换算,由组装层\n * (app-lifecycle)在 MemoryGovernor 回调里调用 `setLevel`,接线放 app 层使\n * headless/serve 同享(不依赖 TUI)。\n * - 所有收紧只影响内存视图(capStoredHistory* 的入参),DB append-forever 不动。\n */\n\nexport type ResidencyPressureLevel = 'healthy' | 'warning' | 'critical'\n\nexport const LEVEL_DIVISOR: Record<ResidencyPressureLevel, number> = {\n healthy: 1,\n warning: 2,\n critical: 4,\n}\n\nexport class ResidencyGovernor {\n private level: ResidencyPressureLevel = 'healthy'\n\n /** 由组装层在 MemoryGovernor 分级回调(onLevelChange 语义)里调用。幂等。 */\n setLevel(level: ResidencyPressureLevel): void {\n this.level = level\n }\n\n get currentLevel(): ResidencyPressureLevel {\n return this.level\n }\n\n /** 当前压力档位下的有效条数上限(base=SESSION_MAX_HISTORY_MESSAGES)。至少 1。 */\n effectiveMaxMessages(base: number): number {\n return Math.max(1, Math.floor(base / LEVEL_DIVISOR[this.level]))\n }\n\n /** 当前压力档位下的有效字节预算(base=SESSION_MAX_CONTENT_BYTES)。至少 1。 */\n effectiveMaxBytes(base: number): number {\n return Math.max(1, Math.floor(base / LEVEL_DIVISOR[this.level]))\n }\n}\n","import type { AgentMessage, TraceEvent } from '@x-otto/agent'\nimport type { AppendLog } from '@x-otto/persistence'\nimport type { SessionSnapshot } from '@x-otto/session'\nimport type { Logger } from '@x-otto/shared'\nimport type { AgentSession } from './agent-session'\n\n/**\n * RFC-305 D3:trace↔DB 崩溃缺口对账。\n *\n * 进程异常终止(SIGKILL/OOM/断电)会让「trace 已记录 turn.end 但 DB 尚未落库」的回合\n * 静默消失。本 detector 在恢复时比对两侧尾部时间戳,检出缺口并缓存供调用方消费。\n *\n * **职责边界(RFC-328 D4)**:只做对账、缓存与「把事实交还给编排层」。\n * - 不自行决定发布什么事件、不碰 UI/toast 策略——它只在检出后调用注入的 `onDetected`\n * 窄回调;发布 owner 唯一且是 `PersistenceSync`(回调实现处)。\n * - 不自动恢复消息、不阻塞 restore、不参与保存链或写租约。\n *\n * 之所以用回调而非「detect 只缓存、consume 时再发布」:既有语义是**检出即发事件**\n * (restore 期间发布,供运行时消费者/审计面),而 `consume` 是给 CLI toast 的值通道,\n * 两者时机不同不可合并。回调让 detector 不持有事件形状,编排层不重算对账逻辑。\n */\n\n/** DB 落库与 trace 写入的容忍尾差(在途写 + 时钟粒度),超出才判定为缺口。 */\nconst CRASH_GAP_TOLERANCE_MS = 10_000\n\n/** 尾窗口扫描上限——只读最近 N 条 trace,避免全量 decode 长会话。 */\nconst CRASH_GAP_TRACE_SCAN_LIMIT = 2000\n\nexport interface CrashGapResult {\n lostTurns: number\n lastPersistedAt: number\n lastTraceAt: number\n}\n\nexport interface CrashGapDetectorDeps {\n /** 缺省(未配置 trace)时 detector 恒为 no-op——对账无输入,不是错误。 */\n traceStore?: AppendLog<TraceEvent>\n logger: Logger\n /**\n * 检出缺口时的窄回调——detector 把事实交还编排层,由后者按既有链路发布。\n * detector 不知道事件形状,也不选择通知策略(RFC-328 D4 的 owner 唯一约束)。\n */\n onDetected?: (agentSession: AgentSession, result: CrashGapResult) => void\n}\n\nexport class CrashGapDetector {\n /** 检出结果缓存;`consume` 一次即删,避免陈旧告警在后续 restore 重复弹出。 */\n private readonly gaps = new Map<string, CrashGapResult>()\n\n constructor(private readonly deps: CrashGapDetectorDeps) {}\n\n /**\n * 对账单个会话。fail-soft:traceStore 缺失/读取异常/无 turn.end → 静默跳过(debug 日志),\n * 绝不抛出、绝不阻塞 restore。最坏退化 = 无告警(等价于该能力未启用)。\n *\n * 必须在 `track()` 之后调用——保持既有订阅时序语义。\n */\n async detect(agentSession: AgentSession, snapshot: SessionSnapshot<AgentMessage>): Promise<void> {\n const store = this.deps.traceStore\n if (!store) return\n\n // DB 侧基准:最后一条 entry 的时间戳。空会话(无 entries)无对账意义。\n // entries 是追加序,取末条即可(原实现全量扫描找 max,长会话是无谓的 O(n))。\n const lastPersistedAt = snapshot.entries[snapshot.entries.length - 1]?.timestamp ?? 0\n if (lastPersistedAt === 0) return\n\n try {\n const total = (await store.count?.(agentSession.id)) ?? undefined\n const range =\n total !== undefined && total > CRASH_GAP_TRACE_SCAN_LIMIT\n ? { from: total - CRASH_GAP_TRACE_SCAN_LIMIT }\n : undefined\n\n let lastTraceAt = 0\n let lostTurns = 0\n for await (const record of store.read(agentSession.id, range)) {\n const ev = record.entry\n if (ev.kind !== 'lifecycle' || ev.node !== 'turn.end') continue\n if (ev.ts > lastPersistedAt + CRASH_GAP_TOLERANCE_MS) {\n lostTurns++\n if (ev.ts > lastTraceAt) lastTraceAt = ev.ts\n }\n }\n if (lostTurns === 0) return\n\n this.deps.logger.warn(\n { sessionId: agentSession.id, lostTurns, lastPersistedAt, lastTraceAt },\n 'RFC-305 D3: crash gap detected — turns recorded in trace but never persisted to DB (lost in previous abnormal termination)',\n )\n const result: CrashGapResult = { lostTurns, lastPersistedAt, lastTraceAt }\n this.gaps.set(agentSession.id, result)\n this.deps.onDetected?.(agentSession, result)\n } catch (error) {\n this.deps.logger.debug?.(\n {\n sessionId: agentSession.id,\n err: error instanceof Error ? error.message : String(error),\n },\n 'RFC-305 D3: crash-gap reconciliation skipped (trace read failed)',\n )\n }\n }\n\n /** 读取并清除检出结果(一次性消费)。 */\n consume(id: string): CrashGapResult | undefined {\n const gap = this.gaps.get(id)\n this.gaps.delete(id)\n return gap\n }\n\n /** 会话移除/驱逐时清理 per-session 状态,防止 Map 残留。 */\n forget(id: string): void {\n this.gaps.delete(id)\n }\n}\n","/**\n * global-residency-engine.ts — RFC-325 M2:全局驻留治理引擎。\n *\n * 这是 RFC-325 的灵魂层:把「当前 process 内所有会话的驻留情况」和「当前可用预算是多少」\n * 的决策集中到一个**确定性、无状态、可单测**的引擎里,取代散在 persistence-sync 里的\n * 碎片化计算。\n *\n * **与现有 `residency-allocator`(D1)的关系**:同一公式,但把 `globalPressure` 显式\n * 作为输入——你不再只能从「会话数」推导预算,还必须从「已经装了多少」推导预算。\n * budget = clamp(totalBudget × (1 − pressure_ratio) / activeCount, MIN, MAX)\n * 这是**负反馈的设计**(驻留越多 → budget 越小),与 `residency-allocator` 的\n * `clamp(totalBudget/N, MIN, MAX)` 在 pressure=0 时等价(pressure=0 时分配完全相同)。\n */\nimport {\n SESSION_TOTAL_RESIDENCY_BUDGET,\n SESSION_MIN_PER_SESSION_BYTES,\n SESSION_MAX_PER_SESSION_BYTES,\n} from '@x-otto/session-contract'\nimport type { ResidencyBudgetConfig } from '@x-otto/session-contract'\n\n/** 引擎运行的快照——调用方提供,引擎不持有状态(RN 严格)。 */\nexport interface EngineSnapshot {\n /** 当前活跃会话数(estimatedBytes > 0 的非 transient 会话)。 */\n activeSessionCount: number\n /** 当前所有会话的总估算字节驻留(来自 InMemorySession.estimatedContentBytes 的汇总)。 */\n currentTotalResidentBytes: number\n /** 压力档除数(来自 ResidencyGovernor,healthy=1 / warning=2 / critical=4)。 */\n pressureDivisor: number\n /** 运行时注入的 residency 配置覆盖。 */\n config?: ResidencyBudgetConfig\n}\n\n/** 引擎产出的治理决策(所有字段确定性)。 */\nexport interface ResidencyDecision {\n /** 单会话当前有效字节预算(已/处分配层 × 压力档)。 */\n maxBytesPerSession: number\n /** 当前全局驻留压力比例(0.0 ~ 1.0)。 */\n globalPressure: number\n /** 是否处于全局超限压力(高压力下才需要紧急治理)。 */\n isGlobalPressure: boolean\n}\n\n// ── 私底封(引擎内部使用,不暴露 — 供 future drivers 重用) ──────────────\n// ─────────────── algorithm ─────────────────────────────────────────────────\n\nconst CAP_LOW_WATERMARK_FRACTION = 0.9 // 滞回下界系数,与你现有逻辑保持相同\n\n/**\n * 把全局总驻留换算为 “压力比例” (0 .. 1)。 有四个语义层:\n * - ratio 解决多会话 shared_budget 的 direct里 at-the-top level pressureシャン\n * - 不回雅芳 clip 1.0 (100%) → 更新it 度限制约:超过 100%mob plus excess_config\n */\nfunction calculatePressureRatio(currentTotalBytes: number, totalBudgetBytes: number): number {\n if (totalBudgetBytes <= 0) return 1\n return Math.min(1, currentTotalBytes / totalBudgetBytes)\n}\n\n/** 全局压力是否达到需要紧急治理的阈值。 */\nconst GLOBAL_PRESSURE_THRESHOLD = 0.85 // 上面 85% 即视为 紧急状态\n// ─────────── 主接口 ───────────────────────────────────────────────────────────\n\n/**\n * 计算“每个会话当前 应该 byte budget — 给定 snapshot,治理一个值。\n *\n * 特性:\n * - 负反馈:currentTotalResidentBytes 越大 pressure 越高 → 每个会话预算*越低*。\n * - 下界 MIN_PER_SESSION:防止分配结果不可用作 (如果 N 极大, SESSION_MAX=50×16=800MB < totalBudget).\n * - 上界 MAX_PER_SESSION:模型折叠输入的最大(imageasher, window by compression)约为 214MB/会话。\n * - 确定性:相同 snapshot → 相同 decision(R4,皱纹是稳定 的。\n */\nexport function decide(snapshot: EngineSnapshot): ResidencyDecision {\n const totalBudget = snapshot.config?.totalBytes ?? SESSION_TOTAL_RESIDENCY_BUDGET\n const minBytes = snapshot.config?.minPerSessionBytes ?? SESSION_MIN_PER_SESSION_BYTES\n const maxBytes = snapshot.config?.maxPerSessionBytes ?? SESSION_MAX_PER_SESSION_BYTES\n\n const pressure = calculatePressureRatio(snapshot.currentTotalResidentBytes, totalBudget)\n const n = Math.max(1, snapshot.activeSessionCount)\n\n // 当压力为 0 时可与旧 residency-allocator 的 clamp(totalBudget/N, MIN, MAX) 完全相同\n const raw = Math.floor((totalBudget * (1 - pressure)) / n)\n const budget = Math.max(minBytes, Math.min(raw, maxBytes))\n\n // 压紧增厚:governor 在 warning/critical 时缩放( 1/2, 1/4) 保持来源可靠\n const scaled = Math.max(Math.floor(minBytes / snapshot.pressureDivisor), Math.floor(budget / snapshot.pressureDivisor))\n\n return {\n maxBytesPerSession: Math.max(1, scaled),\n globalPressure: Number(pressure.toFixed(4)),\n isGlobalPressure: pressure >= GLOBAL_PRESSURE_THRESHOLD,\n }\n}\n\n/** 计算每个会话的低水位滞回目标(第一次裁剪时 retarget 到底 0.9 倍)。 */\nexport function allocationHysteresisTarget(maxBytes: number): number {\n return Math.max(1, Math.floor(maxBytes * CAP_LOW_WATERMARK_FRACTION))\n}\n","/**\n * residency-executor.ts — RFC-325 M3:执行层重构(Driver Registry + 半闸门)。\n *\n * 把 persistence-sync.applyResidencyCaps 里的决策逻辑分离出来:\n * - persistence-sync:只做数据收集(读会话树、计数)和动作执行(调用 cap 方法、发事件)\n * - residency-executor:决定\"当前该做什么\"(压缩前置?裁剪?lossy?)\n *\n * 架构收益:\n * - 决策逻辑独立于持久化机制——可以在不依赖 persistence-sync 的完整生命周期下测试\n * - 每个 driver(触发器)注册自己的判定与动作,新 trigger 类型即插即用\n */\n\nimport type { AgentMessage } from '@x-otto/agent'\nimport type { AgentSession } from './agent-session'\nimport type { ResidencyDecision, EngineSnapshot } from './global-residency-engine'\nimport { decide as decideResidency } from './global-residency-engine'\nimport { InMemorySession } from '@x-otto/session'\n\n/** 执行上下文:当前这轮 cap 评估的所有输入(通常来自 persistence-sync 收集)。 */\nexport interface ExecutorContext {\n /** 目标会话(当前被评估的这个)。 */\n agentSession: AgentSession\n /** 当前活跃会话数(excl. transient,见 countActiveSessionsForAllocation 的同一口径)。 */\n activeSessionCount: number\n /** 当前所有活跃会话的总估算字节驻留(Σ estimatedContentBytes)。 */\n currentTotalResidentBytes: number\n /** 压力档除数(1/2/4)。 */\n pressureDivisor: number\n /** 运行时注入的 residency 配置。 */\n residencyConfig?: import('@x-otto/session-contract').ResidencyBudgetConfig\n /**\n * 生效条数预算(`SESSION_MAX_HISTORY_MESSAGES`,可能是压力档缩放后的值),\n * **由调用方计算并不与字让预算同位**——正资源 driver 边界值不同。\n * Executor 只用字节预算驱动 action;maxMessages 只做 count-side 的二次 cap。\n */\n maxMessages: number\n /**\n * RFC-325:该会话逃生阀压缩**连续失败计数**(用于 lossy 门控)。\n * 由 persistence-sync 注入(per-session 逃逸 valve 状态机的主人),引擎不改。\n */\n lossyFailureCount?: number\n}\n\n/** 一个执行决策——确定性,只由 context 计算。 */\nexport interface ExecutorDecision {\n /** 本轮 caps 的操作。 */\n action: 'none' | 'request-compaction' | 'trim'\n /** 当前生效字节预算(供后续裁剪/事件用)。 */\n maxBytes: number\n /** 是否为有损裁剪(compression 反复失败后放行的兜底)。 */\n isLossy: boolean\n /** 是否因边界保护而提前停止(boundaryLimited 为 true 时)。 */\n boundaryLimited: boolean\n /** 是否触发 activeOversized(裁剪后仍超限,需更多压缩)。 */\n activeOversized: boolean\n}\n\n/** 判定一个 cap 结果是否会触及活跃上下文。 */\nfunction wouldTouchActive(raw: InMemorySession<AgentMessage>, target: number, isByteMode: boolean): boolean {\n if (isByteMode) {\n return raw.wouldByteTrimTouchActiveContext(Math.max(1, target))\n }\n return raw.wouldTrimTouchActiveContext(Math.max(1, target))\n}\n\nconst CAP_LOW_WATERMARK_FRACTION = 0.9\n\n/**\n * RFC-321 R12:逃生阀压缩连续失败多少次后放行**有损裁剪**(裁活跃上下文)。\n *\n * 权衡:压缩失败(LLM 不可用/配额耗尽/摘要异常)时若无限期拒裁,内存会无界增长;\n * 但立刻有损裁剪又会丢模型正在用的上下文。取 3 次——覆盖瞬时故障(网络抖动/限流)\n * 的重试窗口,又不至于让内存在长时间故障下失控。放行时事件带 `lossy: true`(用户可见)。\n *\n * **单一真源**:本常量定义在决策层(executor),persistence-sync 从此处 import。\n * 此前两处各自硬编码 3,一旦漂移会造成\"executor 判定可有损、执行侧仍按旧阈值\"的\n * 撕裂状态——RFC-330 M2 独立审查发现的重复真源。\n */\nexport const LOSSY_TRIM_FAILURE_THRESHOLD = 3\n\n/**\n * RFC-325 D3-A:半闸门逻辑——本轮该做什么。\n *\n * 优先级:\n * 1. 若裁剪会触及活跃上下文且压缩未达失败阈值 → 'request-compaction'(先压缩,本轮跳裁)\n * 2. 若压缩已确认失败(≥3 次)→ 'trim'(lossy 兜底,必须标记)\n * 3. 否则 → 'trim'(正常裁剪——裁的是死历史,不触及活跃上下文)\n */\nexport function decideAction(ctx: ExecutorContext): ExecutorDecision {\n const rawSession = ctx.agentSession.session\n const snapshot: EngineSnapshot = {\n activeSessionCount: ctx.activeSessionCount,\n currentTotalResidentBytes: ctx.currentTotalResidentBytes,\n pressureDivisor: ctx.pressureDivisor,\n config: ctx.residencyConfig,\n }\n const decision: ResidencyDecision = decideResidency(snapshot)\n\n const msgCount = rawSession instanceof InMemorySession ? rawSession.messageCount : 0\n\n const countTarget = Math.max(1, Math.floor(ctx.maxMessages * CAP_LOW_WATERMARK_FRACTION))\n const bytesTarget = Math.max(1, Math.floor(decision.maxBytesPerSession * CAP_LOW_WATERMARK_FRACTION))\n\n if (!(rawSession instanceof InMemorySession)) {\n return { action: 'none', maxBytes: decision.maxBytesPerSession, isLossy: false, boundaryLimited: false, activeOversized: false }\n }\n\n // 低水位只决定“裁到哪里”,绝不提前改变触发阈值;48k < 50k 不得因为 target=45k\n // 而请求压缩/裁剪。否则会破坏 RFC-178 的全值 cap 语义并制造无谓 compaction。\n const countWouldTouch = msgCount > ctx.maxMessages &&\n wouldTouchActive(rawSession, countTarget, false)\n const bytesWouldTouch =\n rawSession.estimatedContentBytes > decision.maxBytesPerSession && wouldTouchActive(rawSession, bytesTarget, true)\n const needsCompactionFirst = countWouldTouch || bytesWouldTouch\n\n // RFC-325 M3:escapeValveFailures 由 persistence-sync 注入(per-session 状态独立于引擎,\n // 引擎无状态只决策——这是 keep our engine pure 的设计约束)。\n const escapeFailures = ctx.lossyFailureCount ?? 0\n const lossyAllowed = escapeFailures >= LOSSY_TRIM_FAILURE_THRESHOLD\n\n if (needsCompactionFirst && !lossyAllowed) {\n return {\n action: 'request-compaction',\n maxBytes: decision.maxBytesPerSession,\n isLossy: false,\n boundaryLimited: false,\n activeOversized: false,\n }\n }\n\n // 无触发(未超限、不需压缩前置)→ 本轮无事\n if (!needsCompactionFirst) {\n const bytesOver = rawSession.estimatedContentBytes > decision.maxBytesPerSession\n const countOver = msgCount > ctx.maxMessages\n if (!bytesOver && !countOver) {\n return {\n action: 'none',\n maxBytes: decision.maxBytesPerSession,\n isLossy: false,\n boundaryLimited: false,\n activeOversized: false,\n }\n }\n }\n\n return {\n action: 'trim',\n maxBytes: decision.maxBytesPerSession,\n isLossy: lossyAllowed && needsCompactionFirst,\n boundaryLimited: false,\n activeOversized: false,\n }\n}\n","import { InMemorySession } from '@x-otto/session'\n\nimport { decideAction, LOSSY_TRIM_FAILURE_THRESHOLD } from './residency-executor'\n\nimport type { AgentMessage } from '@x-otto/agent'\nimport type { Logger } from '@x-otto/shared'\nimport type { ResidencyBudgetConfig } from '@x-otto/session-contract'\nimport type { AgentSession } from './agent-session'\nimport type { ResidencyGovernor, ResidencyPressureLevel } from './residency-governor'\n\n/**\n * RFC-328 D3:驻留动作协调器。\n *\n * 从 `PersistenceSync` 提取「收集输入 → 消费 RFC-325 executor 决策 → 执行动作」这一子域。\n * 提取的判据不是行数,而是**它拥有一组互异于保存链的 per-session 状态机**:\n * 逃生阀失败计数、逃生阀节流基线、在线评估基线、事件发布基线——这四个 Map 的生命周期\n * 只与驻留治理相关,与写租约/保存串行链无关。\n *\n * **职责边界**:\n * - 不重算治理公式——`decideAction` 是唯一动作来源,本类不得用 if/else 覆盖其结论;\n * - 不写 DB、不碰写租约、不拥有保存链——cap 只影响内存视图(RFC-159 append-forever);\n * - 事件发布所需的事实由 `evaluate()` 返回,由调用方沿既有出口发布(发布 owner 唯一)。\n */\n\n/** 低水位裁剪比例——裁到 cap 的 90%,留出滞回空间避免每次 save 都小额裁剪。 */\nconst CAP_LOW_WATERMARK_FRACTION = 0.9\n\nexport interface ResidencyActionResult {\n /** executor 的动作判定(原样透出,便于调用方与测试断言真的走了 executor)。 */\n action: 'none' | 'request-compaction' | 'trim'\n /** 实际裁掉的消息条数(0 = 未裁或裁不动)。 */\n removedCount: number\n /** 本次裁剪是否有损(触及活跃上下文,压缩连续失败后才可能为真)。 */\n lossy: boolean\n /** 是否应发布 `session.history-capped`(已计入滞回发布闸)。 */\n shouldPublish: boolean\n /** 发布所需:触发维度分型。 */\n trigger: 'count' | 'bytes' | 'memory-pressure'\n /** 发布所需:对应维度的 cap 值。 */\n capValue: number\n}\n\nexport interface ResidencyActionCoordinatorDeps {\n logger: Logger\n residencyGovernor?: ResidencyGovernor\n getResidencyConfig?: () => ResidencyBudgetConfig | undefined\n /** 参与预算分摊的活跃会话数(排除 transient)。 */\n countActiveSessions: () => number\n /** 全局总估算驻留字节(含目标会话本身——pressure 是全局已用/总预算)。 */\n sumEstimatedContentBytes: () => number\n /** 请求一次逃生阀压缩(turn-gated,由调用方实现——它拥有 compactionPending 写入权)。 */\n requestCompaction: (agentSession: AgentSession, msgCount: number) => void\n /** 条数上限基准值(调用方注入,避免本类反向依赖 session 常量)。 */\n maxHistoryMessages: number\n /** 在线评估的增量间隔(同时用作逃生阀节流尺度)。 */\n inlineCheckInterval: number\n /**\n * 压力档到分母的映射(governor 档位 → 预算收紧系数)。\n *\n * 类型是 `Record<ResidencyPressureLevel, number>` 而非 `Record<string, number>`——\n * 后者会让\"传入不完整 map\"变成运行期静默兜底(`?? 1` 退化为不收紧预算,压力档失效)\n * 而不是编译期错误。档位是封闭枚举,映射必须全覆盖。\n */\n levelDivisor: Record<ResidencyPressureLevel, number>\n}\n\nexport class ResidencyActionCoordinator {\n /** 在线评估基线:自上次评估以来消息增量达阈值才重新评估。 */\n private readonly evalBaseline = new Map<string, number>()\n /** 事件发布基线:防\"钉在边界上每次 save 都刷 history-capped\"。 */\n private readonly publishBaseline = new Map<string, number>()\n /** 逃生阀压缩连续失败计数——达阈值才放行有损裁剪。 */\n private readonly compactionFailures = new Map<string, number>()\n /** 逃生阀请求节流基线——避免活跃区持续超限期间每次 save 都发 LLM 调用。 */\n private readonly compactionRequestBaseline = new Map<string, number>()\n\n constructor(private readonly deps: ResidencyActionCoordinatorDeps) {}\n\n /**\n * 评估并执行一次驻留动作。返回发布事件所需的事实——**不自行发布**(发布 owner 在调用方)。\n *\n * 非 `InMemorySession`(如远端会话)无内存视图可裁,直接返回 `none`。\n */\n evaluate(agentSession: AgentSession): ResidencyActionResult {\n const none: ResidencyActionResult = {\n action: 'none',\n removedCount: 0,\n lossy: false,\n shouldPublish: false,\n trigger: 'count',\n capValue: 0,\n }\n\n const rawSession = agentSession.session\n if (!(rawSession instanceof InMemorySession)) return none\n\n const id = agentSession.id\n const governor = this.deps.residencyGovernor\n const maxMessages = governor\n ? governor.effectiveMaxMessages(this.deps.maxHistoryMessages)\n : this.deps.maxHistoryMessages\n\n // RFC-325 M2/M3:预算分配与动作判定统一交给 executor;本方法只收集输入并执行其决策。\n const decision = decideAction({\n agentSession,\n activeSessionCount: this.deps.countActiveSessions(),\n currentTotalResidentBytes: this.deps.sumEstimatedContentBytes(),\n pressureDivisor: governor ? this.deps.levelDivisor[governor.currentLevel] : 1,\n residencyConfig: this.deps.getResidencyConfig?.(),\n maxMessages,\n lossyFailureCount: this.compactionFailures.get(id),\n })\n\n const maxBytes = decision.maxBytes\n const canStart = (m: AgentMessage) => m.role !== 'tool_result'\n const msgCount = rawSession.messageCount\n\n if (decision.action === 'request-compaction') {\n this.deps.requestCompaction(agentSession, msgCount)\n this.deps.logger.debug(\n { sessionId: id, msgCount },\n '[cap] trim would touch active context; requested compaction first, skipping trim this round (RFC-321 R12)',\n )\n this.evalBaseline.set(id, msgCount)\n return { ...none, action: 'request-compaction' }\n }\n\n if (decision.action === 'none') {\n this.evalBaseline.set(id, msgCount)\n return none\n }\n\n const lossyMode = decision.isLossy\n // activeOversized 的重试门只看压缩失败阈值;普通无损 trim 不应被误判为耗尽。\n const compactionExhausted =\n (this.compactionFailures.get(id) ?? 0) >= LOSSY_TRIM_FAILURE_THRESHOLD\n\n const countTarget = Math.max(1, Math.floor(maxMessages * CAP_LOW_WATERMARK_FRACTION))\n const bytesTarget = Math.max(1, Math.floor(maxBytes * CAP_LOW_WATERMARK_FRACTION))\n\n const countResult =\n msgCount > maxMessages\n ? rawSession.capStoredHistory(countTarget, canStart)\n : { capped: false, removedCount: 0, boundaryLimited: false, activeOversized: false }\n const bytesResult =\n rawSession.estimatedContentBytes > maxBytes\n ? rawSession.capStoredHistoryByBytes(bytesTarget, canStart)\n : { capped: false, removedCount: 0, boundaryLimited: false, activeOversized: false }\n\n const removedCount = countResult.removedCount + bytesResult.removedCount\n const lossy = lossyMode && removedCount > 0\n\n // 进入有损路径即重置失败计数(而非仅当真裁了才重置)——\"裁不动\"场景\n // (死历史为 0、边界在最前)removedCount=0,若挂在 removedCount>0 上则计数永不重置\n // → 压缩重试永久停摆。\n if (lossyMode) this.compactionFailures.delete(id)\n\n if (countResult.boundaryLimited || bytesResult.boundaryLimited) {\n this.deps.logger.debug(\n {\n sessionId: id,\n removedCount,\n msgCount,\n activeOversized: countResult.activeOversized || bytesResult.activeOversized,\n },\n '[cap] trimming stopped at active compaction boundary (RFC-321 R4); may remain above cap',\n )\n }\n\n // RFC-321 D1-a′ 逃生阀:边界前死历史已裁尽、活跃区自身仍超限——再裁就要吃穿边界(R4\n // 禁止),唯一出路是压缩活跃区本身。达失败阈值后停止自动重试(LLM 故障期的成本/限流\n // 灾难防护),只保留 lossy 裁剪兜底。\n if ((countResult.activeOversized || bytesResult.activeOversized) && !compactionExhausted) {\n this.deps.requestCompaction(agentSession, msgCount)\n this.deps.logger.debug(\n { sessionId: id, msgCount, bytes: rawSession.estimatedContentBytes },\n '[cap] active region itself exceeds cap (RFC-321 activeOversized); compaction needed',\n )\n }\n\n const lastPublished = this.publishBaseline.get(id)\n const publishGrowthGate = Math.max(\n 1,\n Math.floor(maxMessages * (1 - CAP_LOW_WATERMARK_FRACTION)),\n )\n // lossy 事件不受发布闸压制——有损裁剪意味着模型上下文被真实削减(用户有权每次知道),\n // 且它本就罕见(压缩连续失败 3 次才可达),不构成刷屏风险。\n const suppressedByGate =\n !lossy && lastPublished !== undefined && msgCount < lastPublished + publishGrowthGate\n const shouldPublish = removedCount > 0 && !suppressedByGate\n\n // RFC-178 D6:触发来源分型。压力收紧态下任何裁剪归因 memory-pressure(cap 被系数\n // 压低才裁的概率主导);healthy 下按主要贡献维度分型。\n const trigger: 'count' | 'bytes' | 'memory-pressure' =\n governor && governor.currentLevel !== 'healthy'\n ? 'memory-pressure'\n : bytesResult.removedCount > countResult.removedCount\n ? 'bytes'\n : 'count'\n\n if (shouldPublish) this.publishBaseline.set(id, msgCount)\n this.evalBaseline.set(id, msgCount - removedCount)\n\n return {\n action: 'trim',\n removedCount,\n lossy,\n shouldPublish,\n trigger,\n capValue: trigger === 'bytes' ? maxBytes : maxMessages,\n }\n }\n\n /**\n * 在线评估门:消息增量达阈值才真正评估。返回 `true` 表示调用方应执行 `evaluate()`。\n * 首次调用只登记基线不评估(避免恢复的长会话在 track 后立刻裁剪)。\n */\n shouldEvaluateInline(agentSession: AgentSession): boolean {\n const rawSession = agentSession.session\n if (!(rawSession instanceof InMemorySession)) return false\n\n const msgCount = rawSession.messageCount\n const baseline = this.evalBaseline.get(agentSession.id)\n if (baseline === undefined) {\n this.evalBaseline.set(agentSession.id, msgCount)\n return false\n }\n return msgCount - baseline >= this.deps.inlineCheckInterval\n }\n\n /** 逃生阀请求节流:返回 `true` 表示本次请求应放行。 */\n shouldRequestCompaction(sessionId: string, msgCount: number): boolean {\n const last = this.compactionRequestBaseline.get(sessionId)\n if (last !== undefined && msgCount < last + this.deps.inlineCheckInterval) return false\n this.compactionRequestBaseline.set(sessionId, msgCount)\n return true\n }\n\n /** 压缩成功——清零失败计数(以真的产生了边界为准,见调用方注释)。 */\n onCompactionSucceeded(sessionId: string): void {\n this.compactionFailures.delete(sessionId)\n }\n\n /** 压缩失败(或调用成功但未产生边界)——累计失败,达阈值后放行有损裁剪。 */\n onCompactionFailed(sessionId: string): number {\n const next = (this.compactionFailures.get(sessionId) ?? 0) + 1\n this.compactionFailures.set(sessionId, next)\n return next\n }\n\n /** 会话移除/驱逐时清理全部 per-session 状态,防止 Map 残留。 */\n forget(sessionId: string): void {\n this.evalBaseline.delete(sessionId)\n this.publishBaseline.delete(sessionId)\n this.compactionFailures.delete(sessionId)\n this.compactionRequestBaseline.delete(sessionId)\n }\n}\n","import type { AgentMessage } from '@x-otto/agent'\nimport type {\n DraftEntry,\n EditedFile,\n InMemorySession,\n SessionSnapshot,\n SubagentEntry,\n TodoItem,\n TurnSummaryEntry,\n} from '@x-otto/session'\nimport type { Logger } from '@x-otto/shared'\nimport type { PanelStateStore } from './panel-state-store'\n\n/**\n * RFC-328 D4:面板态恢复与遗留 metadata 一次性迁移。\n *\n * 从 `PersistenceSync` 提取的独立恢复子域——它有明确的三段式流程\n * (独立存储优先 → 旧 metadata 兼容读取 → 回读验证后一次性迁移),与保存链、写租约、\n * 驻留治理全部正交。\n *\n * **职责边界**:\n * - 只写 `PanelStateStore` 与内存 session 的 panel setter,**不碰主快照、不碰写租约**;\n * - 迁移必须先通过验证锁(回读比对)才落盘——迁移逻辑有 bug 时宁可只留内存态并告警,\n * 也不写入可能损坏的数据;\n * - `panelStateStore` 缺省时整体降级为 no-op(面板态仅存在于内存,重启后丢失但不报错)。\n */\n\n/**\n * 结构深比较(验证锁的元素级比对)。\n *\n * 不用 `JSON.stringify(a) === JSON.stringify(b)`——那对键序敏感,同内容不同插入顺序的\n * 对象会被误判为不一致,导致验证锁假阴性、静默跳过应有的持久化(数据仍在内存但不落\n * `PanelStateStore`)。这里是键序无关的递归比较,不引入序列化开销。\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (typeof a !== typeof b) return false\n if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false\n if (Array.isArray(a) !== Array.isArray(b)) return false\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((item, i) => deepEqual(item, b[i]))\n }\n const aObj = a as Record<string, unknown>\n const bObj = b as Record<string, unknown>\n const aKeys = Object.keys(aObj)\n const bKeys = Object.keys(bObj)\n if (aKeys.length !== bKeys.length) return false\n return aKeys.every((key) => Object.hasOwn(bObj, key) && deepEqual(aObj[key], bObj[key]))\n}\n\n/** 深比较两个数组。两者均 undefined → 一致;一者 undefined 另一者不是 → 不一致。 */\nfunction deepCompareArrayOrUndef<T>(a: T[] | undefined, b: T[] | undefined): boolean {\n if (a === undefined && b === undefined) return true\n if (a === undefined || b === undefined) return false\n if (a.length !== b.length) return false\n return deepEqual(a, b)\n}\n\nexport interface PanelStateRestorerDeps {\n /** 缺省时整体 no-op——面板态仅存在于内存,重启后丢失但不报错崩溃。 */\n panelStateStore?: PanelStateStore\n logger: Logger\n}\n\nexport class PanelStateRestorer {\n constructor(private readonly deps: PanelStateRestorerDeps) {}\n\n /**\n * RFC-108 D3/D4:恢复面板态。\n *\n * 优先从 `PanelStateStore` 读取独立存储;无数据则从旧快照 `metadata.config` 运行时对象\n * 迁移遗留字段(类型层面已移除声明,但 `JSON.parse` 出的旧数据在运行时仍可能残留这些\n * key)。迁移通过验证锁后才写入独立存储。\n */\n async restore(\n rawSession: InMemorySession<AgentMessage>,\n snapshot: SessionSnapshot<AgentMessage>,\n id: string,\n ): Promise<void> {\n const store = this.deps.panelStateStore\n if (!store) return\n\n const panelSnapshot = await store.loadAll(id)\n if (panelSnapshot) {\n // 独立存储中已有数据,直接用同步 setter 填充内存\n if (panelSnapshot.todoList !== undefined) {\n rawSession.setTodoList(panelSnapshot.todoList as unknown as TodoItem[])\n }\n if (panelSnapshot.editedFiles !== undefined) {\n rawSession.setEditedFiles(panelSnapshot.editedFiles as unknown as EditedFile[])\n }\n if (panelSnapshot.subagents !== undefined) {\n rawSession.setSubagents(panelSnapshot.subagents as unknown as SubagentEntry[])\n }\n if (panelSnapshot.drafts !== undefined) {\n rawSession.setDrafts(panelSnapshot.drafts as unknown as DraftEntry[])\n }\n // turnSummaries 是后续新增字段,从未出现在旧 metadata.config 里,无需下方一次性\n // 迁移兜底路径——只从独立存储读取,无数据时保持 undefined(resume 侧按空数组处理)。\n if (panelSnapshot.turnSummaries !== undefined) {\n rawSession.setTurnSummaries(panelSnapshot.turnSummaries as unknown as TurnSummaryEntry[])\n }\n return\n }\n\n // 一次性迁移兜底:旧快照 metadata.config 运行时对象上可能仍有这 4 个字段\n const legacyConfig = snapshot.metadata.config as Record<string, unknown> | undefined\n const legacyTodoList = legacyConfig?.todoList as TodoItem[] | undefined\n const legacyEditedFiles = legacyConfig?.editedFiles as EditedFile[] | undefined\n const legacySubagents = legacyConfig?.subagents as SubagentEntry[] | undefined\n const legacyDrafts = legacyConfig?.drafts as DraftEntry[] | undefined\n\n if (!legacyTodoList && !legacyEditedFiles && !legacySubagents && !legacyDrafts) return\n\n // 填充内存\n if (legacyTodoList) rawSession.setTodoList(legacyTodoList)\n if (legacyEditedFiles) rawSession.setEditedFiles(legacyEditedFiles)\n if (legacySubagents) rawSession.setSubagents(legacySubagents)\n if (legacyDrafts) rawSession.setDrafts(legacyDrafts)\n\n // 验证锁:回读比对,防迁移逻辑 bug 导致数据不可恢复\n const isConsistent =\n deepCompareArrayOrUndef(legacyTodoList, rawSession.getTodoList()) &&\n deepCompareArrayOrUndef(legacyEditedFiles, rawSession.getEditedFiles()) &&\n deepCompareArrayOrUndef(legacySubagents, rawSession.getSubagents()) &&\n deepCompareArrayOrUndef(legacyDrafts, rawSession.getDrafts())\n\n if (isConsistent) {\n await store.saveAll(id, {\n sessionId: id,\n todoList: legacyTodoList ?? [],\n editedFiles: legacyEditedFiles ?? [],\n subagents: legacySubagents ?? [],\n drafts: legacyDrafts ?? [],\n })\n } else {\n this.deps.logger.warn(\n { sessionId: id },\n 'PanelState migration verification failed, using legacy in-memory data without persisting',\n )\n }\n }\n}\n","import { normalizePasteSnapshot } from '@x-otto/persistence'\nimport type {\n PanelStatePersistence,\n PanelStateSnapshot,\n PanelStateTodoItem,\n PanelStateEditedFile,\n PanelStateSubagentEntry,\n PanelStateDraftEntry,\n PanelStateTurnSummary,\n PasteStatePersistence,\n PasteStateSnapshot,\n} from '@x-otto/persistence'\n\n/**\n * RFC-108 D3:面板 UI 状态读写门面。\n *\n * 内部调用 `PanelStatePersistence` 的 `load`/`save` 做 per-session 的四个面板\n * 字段存取。每个 get 方法 load 后取对应字段返回;每个 set 方法先 load 现有快照\n * (若无则初始化空对象)、更新对应字段、再 save。\n *\n * 当前为简单实现,不做额外缓存层——每次 get/set 都直接走持久化后端。\n * 未来可根据 profiling 数据决定是否需要内存缓存层。\n *\n * RFC-338:paste 缓存经**独立的** `pasteStatePersistence` 存取(见 getPaste/setPaste),\n * 不进 `PanelStateSnapshot`——原因见下方方法注释与 paste-state.ts 头注释。\n */\nexport class PanelStateStore {\n constructor(\n private readonly persistence: PanelStatePersistence,\n /**\n * RFC-338:paste 缓存的独立后端。可选——未注入时 paste 存取降级为\n * 「读恒空 / 写 no-op」,与 in-memory/remote 后端不构造 PanelStateStore 的\n * 既有降级策略同构(不 throw,不阻断输入)。\n */\n private readonly pasteStatePersistence?: PasteStatePersistence,\n ) {}\n\n // ---- todoList ----\n\n async getTodoList(sessionId: string): Promise<PanelStateTodoItem[] | undefined> {\n const snapshot = await this.persistence.load(sessionId)\n return snapshot?.todoList\n }\n\n async setTodoList(sessionId: string, todos: PanelStateTodoItem[]): Promise<void> {\n const snapshot = await this.loadOrInit(sessionId)\n snapshot.todoList = todos\n await this.persistence.save(snapshot)\n }\n\n // ---- editedFiles ----\n\n async getEditedFiles(sessionId: string): Promise<PanelStateEditedFile[] | undefined> {\n const snapshot = await this.persistence.load(sessionId)\n return snapshot?.editedFiles\n }\n\n async setEditedFiles(sessionId: string, files: PanelStateEditedFile[]): Promise<void> {\n const snapshot = await this.loadOrInit(sessionId)\n snapshot.editedFiles = files\n await this.persistence.save(snapshot)\n }\n\n // ---- subagents ----\n\n async getSubagents(sessionId: string): Promise<PanelStateSubagentEntry[] | undefined> {\n const snapshot = await this.persistence.load(sessionId)\n return snapshot?.subagents\n }\n\n async setSubagents(sessionId: string, entries: PanelStateSubagentEntry[]): Promise<void> {\n const snapshot = await this.loadOrInit(sessionId)\n snapshot.subagents = entries\n await this.persistence.save(snapshot)\n }\n\n // ---- drafts ----\n\n async getDrafts(sessionId: string): Promise<PanelStateDraftEntry[] | undefined> {\n const snapshot = await this.persistence.load(sessionId)\n return snapshot?.drafts\n }\n\n async setDrafts(sessionId: string, entries: PanelStateDraftEntry[]): Promise<void> {\n const snapshot = await this.loadOrInit(sessionId)\n snapshot.drafts = entries\n await this.persistence.save(snapshot)\n }\n\n // ---- turnSummaries ----\n\n async getTurnSummaries(sessionId: string): Promise<PanelStateTurnSummary[] | undefined> {\n const snapshot = await this.persistence.load(sessionId)\n return snapshot?.turnSummaries\n }\n\n async setTurnSummaries(sessionId: string, entries: PanelStateTurnSummary[]): Promise<void> {\n const snapshot = await this.loadOrInit(sessionId)\n snapshot.turnSummaries = entries\n await this.persistence.save(snapshot)\n }\n\n // ---- paste(RFC-338,独立后端)----\n\n /**\n * RFC-338:读取 paste 缓存。\n *\n * **不经 `loadOrInit`、不碰 `PanelStateSnapshot`**——这正是 D1 的要点:\n * 面板态的每个 setter 都是「全量 load → 改一字段 → 全量 save」,而\n * `setEditedFiles` 每次文件编辑就触发一次。若 paste(最大 8MB)与面板态同处\n * 一份 JSON,每次编辑都要多序列化一遍这 8MB。两者物理分离后互不拖累。\n *\n * 返回值恒为规范化后的合法状态(fail-open):后端未注入、文件缺失、内容损坏、\n * 字段非法一律降级为空状态而非抛错——粘贴缓存丢失只该让 chip 失效(有 toast\n * 提示的安全失败),绝不该让会话打不开。\n */\n async getPaste(sessionId: string): Promise<PasteStateSnapshot> {\n if (!this.pasteStatePersistence) return { sessionId, entries: [], nextId: 1 }\n try {\n const raw = await this.pasteStatePersistence.load(sessionId)\n return normalizePasteSnapshot(sessionId, raw)\n } catch {\n // 损坏 JSON 等:DiskPersistence 会抛 PersistenceError,此处兜住。\n return { sessionId, entries: [], nextId: 1 }\n }\n }\n\n /**\n * RFC-338:写入 paste 缓存(全量覆写,非 merge)。\n *\n * 调用方持有内存中的权威 LRU 顺序,整批写入即可。\n * fail-soft:落盘失败吞错——粘贴/提交是用户主路径,不能因缓存写盘失败被阻断。\n *\n * @returns 是否真正落盘成功(供调用方按需记日志;不影响主流程)\n */\n async setPaste(sessionId: string, snapshot: PasteStateSnapshot): Promise<boolean> {\n if (!this.pasteStatePersistence) return false\n if (snapshot.sessionId !== sessionId) {\n // 与 saveAll 同款防御:串号写入是会话归属错配(RFC-338 D7/D9 的核心风险),\n // fail-fast 而非静默错写。\n throw new Error(\n `PanelStateStore.setPaste: snapshot.sessionId (${snapshot.sessionId}) !== sessionId (${sessionId})`,\n )\n }\n try {\n await this.pasteStatePersistence.save(snapshot)\n return true\n } catch {\n return false\n }\n }\n\n /** RFC-338:删除某会话的 paste 缓存(会话删除时的连带清理)。 */\n async deletePaste(sessionId: string): Promise<boolean> {\n if (!this.pasteStatePersistence) return false\n try {\n return await this.pasteStatePersistence.delete(sessionId)\n } catch {\n return false\n }\n }\n\n // ---- internal ----\n\n /**\n * 批量加载——直接返回 `PanelStateSnapshot`,供 persistence-sync 恢复/迁移路径使用。\n * 比分别调用 4 次 get 方法更高效(一次 load 而非四次)。\n */\n async loadAll(sessionId: string): Promise<PanelStateSnapshot | null> {\n return this.persistence.load(sessionId)\n }\n\n /**\n * 批量保存——全量覆写(非 merge),供 persistence-sync 落盘路径使用。\n * 调用方(doSave/saveSnapshot)总是读取 live session 的全部 4 个 getter 值后整批写入,\n * 故无需与已有持久化数据做 per-field merge。\n * RFC-118 小修(review 观察项):sessionId 参数此前被忽略(`_` 前缀),API 不对称——\n * 改为防御性校验,snapshot 串号(写 A 会话数据到 B 键)时 fail-fast 而非静默错写。\n */\n async saveAll(sessionId: string, snapshot: PanelStateSnapshot): Promise<void> {\n if (snapshot.sessionId !== sessionId) {\n throw new Error(\n `PanelStateStore.saveAll: snapshot.sessionId (${snapshot.sessionId}) !== sessionId (${sessionId})`,\n )\n }\n await this.persistence.save(snapshot)\n }\n\n private async loadOrInit(sessionId: string): Promise<PanelStateSnapshot> {\n const existing = await this.persistence.load(sessionId)\n return existing ?? { sessionId }\n }\n}\n","import { InMemorySession, RemoteSnapshotConflictError } from '@x-otto/session'\nimport { SESSION_SNAPSHOT_VERSION } from '@x-otto/session'\nimport { SESSION_MAX_HISTORY_MESSAGES } from '@x-otto/session-contract'\nimport { ResidencyGovernor } from './residency-governor'\nimport { LEVEL_DIVISOR } from './residency-governor'\n\nimport type {\n PersistedSessionConfig,\n SessionPersistence,\n SessionSnapshot,\n} from '@x-otto/session'\nimport { CrashGapDetector } from './crash-gap-detector'\nimport { ResidencyActionCoordinator } from './residency-action-coordinator'\nimport { PanelStateRestorer } from './panel-state-restorer'\n\nimport type { AgentMessage, TraceEvent } from '@x-otto/agent'\nimport type { AppendLog } from '@x-otto/persistence'\nimport type { Logger } from '@x-otto/shared'\nimport type { AgentSession } from './agent-session'\nimport type { ResolvedSessionConfig } from './types'\nimport { PanelStateStore } from './panel-state-store'\n\n// RFC-155 M155-1:SESSION_MAX_HISTORY_MESSAGES 归口到 @x-otto/session-contract(详见该文件\n// constants.ts 头注释)——真实消费方分处 @x-otto/runtime(本文件)与 @x-otto/tui(context-store.ts),\n// 两者均已依赖 session-contract,归口于此零新增依赖边,且支持 OTTO_SESSION_MAX_HISTORY_MESSAGES\n// 环境变量覆盖(本文件不再本地定义该常量,此前的\"不可调小\"注释已迁移至 constants.ts 详述)。\n\n/**\n * RFC-305 D1(2026-07-31,取代 2026-07-12 的 30s/5turn 节流 P2 缓解):turn.end 每次\n * 直接入串行链落 entries 增量(doSaveEntries,不含 panelState),把长工具循环内的 DB\n * 崩溃窗口从\"最近 30 秒或 5 turn\"收窄到\"单条入链在途(毫秒级)\"。\n *\n * 为什么不需要节流(RFC-305 §3 D1 论证):entries 落库是游标增量 append(O(新消息),\n * relational-session-persistence.ts already-cursor 设计),WAL 模式 SQLite 单事务毫秒级;\n * 一个 turn 天然 ≥ 数百 ms(含模型往返),频率有物理上限。panelState(每次全量重写 5 个\n * 存储)不随 turn 落盘,维持 prompt.end 节奏(doSaveFull,RFC-305 D2 拆档)。\n *\n * 为什么不 await(RFC-305 评审 F2):enqueue 串行链保证同会话按序,fire-and-forget 下\n * 工作循环零阻塞;await 只会白付延迟,不再缩小任何窗口。\n *\n * trace 不可作恢复兜底(RFC-102 D1 既有决策):tool.call.end 只存压缩摘要,反向重建有损\n * ——崩溃丢失的\"可见性\"由 restore 时 trace↔DB 对账负责(RFC-305 D3),不做自动恢复。\n *\n * RFC-305 M3(后端能力自适应):turn 级落库策略由持久化后端能力声明(persistence.turnFlush)\n * 决定,不是用户配置:\n * - 'incremental'(缺省,本地 relational SQLite):每 turn 落库(本文件头 D1 论证不变)。\n * - 'snapshot'(远端 PUT 整份快照 / 旧 blob 逃生口整份覆写):turn 级节流(下常量),\n * prompt.end 仍强制落库——避免每 turn 全量上传随会话线性膨胀。节流只是把丢失窗口\n * 从毫秒级放宽回\"最近 N turn/秒\"(后端能力边界,非策略缺陷),crash-gap 对账照常。\n */\n\n/**\n * RFC-178 D5(收编 RFC-147 E-2):在线 cap 评估的 append 间隔——自上次 cap 评估以来\n * 消息 append 计数超过本值时,即在下一次 append 后评估驻留裁剪(不等 doSave 节奏)。\n * 推导:200 条 × 典型 2KB/条 ≈ 400KB 在线窗口增量(相对 256MB 预算 0.16% 可忽略),\n * 小于条数触发因子的余量(5000×0.1=500 条)——评估间隔不吞掉触发因子安全窗口。\n * 每条都评估是浪费(cap 判定 O(n) 数条数),本间隔把最坏无界窗口收敛到有界。\n */\nconst CAP_INLINE_CHECK_INTERVAL = 200\n\n// RFC-328 D3:cap 滞回低水位常量已随裁剪逻辑迁入 residency-action-coordinator。\n\n// RFC-321 R12 的 lossy 放行阈值单一真源在 residency-executor(决策层),\n// 本文件从那里 import——此前两处各自硬编码 3,是 RFC-330 M2 审查发现的重复真源。\n\n// RFC-328 M1:crash-gap 的容忍窗口与扫描上限已随对账逻辑迁入 `crash-gap-detector.ts`\n// (常量与其唯一消费者同处,避免跨文件的隐式耦合)。\n\n/**\n * RFC-305 M3:snapshot 后端(远端 PUT / 旧 blob 逃生口)的 turn 级节流阈值——\n * 30s/5 turn 先满足者触发(与 2026-07-12 的 P2 缓解同值,语义已改为\"后端能力驱动\")。\n * incremental 后端不走此路径(每 turn 落库)。\n */\nconst TURN_SAVE_THROTTLE_MS = 30_000\nconst TURN_SAVE_THROTTLE_COUNT = 5\n\nexport interface PersistenceSyncDeps {\n persistence: SessionPersistence<AgentMessage>\n logger: Logger\n /** disk/remote 后端 = true(prompt 终态增量落盘);in-memory 缺省 false。 */\n autoPersist: boolean\n getSession: (id: string) => AgentSession | undefined\n hasSession: (id: string) => boolean\n listSessionIds: () => Iterable<string>\n /** 容量预检(含 idle 驱逐 + 满池 LRU 驱逐)。restore 单条不足时抛错。 */\n canAccommodate: (id: string) => boolean\n /** 剩余容量(纯查询,无驱逐副作用)。restoreAll 用它截断入池数量,不挤掉在池会话。 */\n capacityLeft: () => number\n /** restore 路径池登记:构建 AgentSession + 登记进池(不改 active;track 由本类负责)。 */\n adoptRestoredSession: (\n raw: InMemorySession<AgentMessage>,\n config: ResolvedSessionConfig,\n ) => Promise<AgentSession>\n /** restoreAll 的 active 选举(恢复后无 active 时设为最近活跃者)。 */\n setActiveIfNone: (id: string) => void\n /** 仅 restore/restoreAll 使用;未提供则两方法分别返回 null / 0。 */\n /** restore 模板工厂。传持久化 agentName 以重解析子代理 prompt 身份(缺省=main)。 */\n configProvider?: (agentName?: string, modelId?: string) => ResolvedSessionConfig\n /** save 时取会话当前解析后配置,抽取 per-session 子集入快照 metadata。 */\n getConfig?: (id: string) => ResolvedSessionConfig | undefined\n /**\n * RFC-178 触发器 C:驻留收紧系数源(可选——未提供时恒用全值 cap,行为与此前一致)。\n * 由组装层(app-lifecycle)把 MemoryGovernor 分级信号写入其 setLevel。\n */\n residencyGovernor?: ResidencyGovernor\n /**\n * RFC-323 M4:驻留预算运行时配置(settings.json 覆盖经此注入,缺省回退 env/常量)。\n * 惰性 getter——app 层 ensureSettingsLoaded 后才有值,故每次读而非构造期快照。\n */\n getResidencyConfig?: () => import('@x-otto/session-contract').ResidencyBudgetConfig | undefined\n /**\n * RFC-108 D3:面板态独立持久化门店(可选——未提供时降级为不独立持久化,\n * 面板态仅存在于内存,重启后丢失但不报错崩溃)。\n */\n panelStateStore?: PanelStateStore\n /**\n * RFC-305 D3:trace↔DB 崩溃对账数据源(可选——未提供时对账整体跳过,restore 行为\n * 与此前一致)。trace append-log 逐事件实时落盘(独立于 session DB 的 save 链),\n * 崩溃时 trace 记录的 turn 覆盖领先于 DB——restore 时比对可检出\"已发生未落库\"缺口。\n */\n traceStore?: AppendLog<TraceEvent>\n /**\n * RFC-305 M3:持久化后端 turn-flush 能力(缺省 'incremental',由装配层透传\n * persistence.turnFlush)。'snapshot' 后端(远端 PUT 整份快照/旧 blob 逃生口整份覆写)\n * 对 turn 级落库节流,'incremental'(本地 relational 游标增量)每 turn 落库。\n * 这是后端能力事实,不是用户配置——未来远端支持 append-tail 后改声明即可自动升级。\n */\n turnFlush?: 'incremental' | 'snapshot'\n}\n\n/** 抽取可持久化的 per-session 配置子集(全 primitive)。 */\nexport function pickPersistableConfig(config: ResolvedSessionConfig): PersistedSessionConfig {\n const out: PersistedSessionConfig = {}\n if (config.interactive !== undefined) out.interactive = config.interactive\n if (config.depth !== undefined) out.depth = config.depth\n if (config.agentName !== undefined) out.agentName = config.agentName\n if (config.thinkingLevel !== undefined) out.thinkingLevel = config.thinkingLevel\n if (config.maxToolTurnExtensions !== undefined)\n out.maxToolTurnExtensions = config.maxToolTurnExtensions\n if (config.transient !== undefined) out.transient = config.transient\n if (config.model?.id) out.modelId = config.model.id\n if (config.systemTail !== undefined) out.systemTail = config.systemTail\n if (config.workspaceDir !== undefined) out.workspaceDir = config.workspaceDir\n if (config.maxToolTurns !== undefined) out.maxToolTurns = config.maxToolTurns\n if (config.permissionAlwaysAllow && config.permissionAlwaysAllow.length > 0) {\n out.permissionAlwaysAllow = [...config.permissionAlwaysAllow]\n }\n if (config.availableModels && config.availableModels.length > 0) {\n out.availableModels = [...config.availableModels]\n }\n return out\n}\n\n// RFC-328 D4:验证锁的深比较 helper 已随面板态迁移逻辑迁入 panel-state-restorer.ts\n// (helper 与其唯一消费者同处,避免留在本文件成为间接孤儿)。\n\n/** restore 时把快照里的 per-session 配置覆盖到共享模板上(缺失字段回退模板)。 */\nexport function applyPersistedConfig(\n template: ResolvedSessionConfig,\n persisted: PersistedSessionConfig | undefined,\n): ResolvedSessionConfig {\n if (!persisted) return template\n const merged: ResolvedSessionConfig = { ...template }\n if (persisted.interactive !== undefined) merged.interactive = persisted.interactive\n if (persisted.depth !== undefined) merged.depth = persisted.depth\n if (persisted.agentName !== undefined) merged.agentName = persisted.agentName\n if (persisted.thinkingLevel !== undefined) {\n merged.thinkingLevel = persisted.thinkingLevel as ResolvedSessionConfig['thinkingLevel']\n }\n if (persisted.maxToolTurnExtensions !== undefined) {\n merged.maxToolTurnExtensions = persisted.maxToolTurnExtensions\n }\n if (persisted.transient !== undefined) merged.transient = persisted.transient\n if (persisted.systemTail !== undefined) merged.systemTail = persisted.systemTail\n if (persisted.workspaceDir !== undefined) merged.workspaceDir = persisted.workspaceDir\n if (persisted.maxToolTurns !== undefined) merged.maxToolTurns = persisted.maxToolTurns\n if (persisted.permissionAlwaysAllow !== undefined)\n merged.permissionAlwaysAllow = [...persisted.permissionAlwaysAllow]\n if (persisted.availableModels !== undefined)\n merged.availableModels = [...persisted.availableModels]\n return merged\n}\n\nexport class PersistenceSync {\n private readonly saveChains = new Map<string, Promise<void>>()\n /**\n * RFC-328 D4:崩溃缺口对账已提取为独立可替换单元(`CrashGapDetector`)。\n * 本类保留**唯一发布 owner** 身份:detector 只缓存事实,事件/提示由 `consumeCrashGap()`\n * 沿既有恢复通知链发出——恢复通知层不得绕过本类直接读 detector。\n */\n private readonly crashGapDetector: CrashGapDetector\n /**\n * RFC-328 D4:面板态恢复与遗留 metadata 迁移已提取为独立单元。\n * 它只写 PanelStateStore 与内存 panel setter,不碰主快照/写租约。\n */\n private readonly panelStateRestorer: PanelStateRestorer\n /** RFC-305 M3:snapshot 后端 per-session turn 节流状态(仅 turnFlush='snapshot' 使用)。 */\n private readonly turnSaveState = new Map<string, { lastSaveAt: number; turnsSinceSave: number }>()\n /**\n * 2026-07-14:per-session \"此前是否处于写租约被拒态\"标记——用于探测\"denied→恢复可写\"\n * 的状态转换,据此 publish 对称的 session.write-lease-restored 事件(仅在真实发生\n * 状态转换时发一次,不随每次成功 save 重复发)。会话销毁/驱逐时清理\n * (forgetSession/flushPending),避免无限期残留。\n */\n private readonly leaseDeniedState = new Map<string, boolean>()\n /** RFC-178 D5:per-session 上次在线 cap 评估时的消息条数基准(在线间隔判定用)。 */\n /**\n * M1 远端权威:per-session 已知的服务端 revision。每次 CAS 成功后更新;\n * 409 reload/rebase 后用最新 revision 重 PUT。本地后端不使用此字段。\n */\n private readonly remoteRevisions = new Map<string, number>()\n /**\n * RFC-328 D3:驻留动作的输入收集、executor 消费与动作执行已提取为\n * `ResidencyActionCoordinator`——它拥有在线评估基线、发布基线、压缩失败计数、\n * 逃生阀节流基线这四个**只与驻留治理相关**的 per-session 状态机。\n *\n * 本类保留 `session.history-capped` 的**唯一发布 owner** 身份:coordinator 只返回\n * 发布所需事实,事件由 `applyResidencyCaps` 沿既有出口发出。\n */\n private readonly residencyCoordinator: ResidencyActionCoordinator\n\n constructor(private readonly deps: PersistenceSyncDeps) {\n this.residencyCoordinator = new ResidencyActionCoordinator({\n logger: deps.logger,\n ...(deps.residencyGovernor ? { residencyGovernor: deps.residencyGovernor } : {}),\n ...(deps.getResidencyConfig ? { getResidencyConfig: deps.getResidencyConfig } : {}),\n countActiveSessions: () => this.countActiveSessionsForAllocation(),\n sumEstimatedContentBytes: () => this.sumEstimatedContentBytes(),\n // 逃生阀压缩由本类实现——它拥有 AgentSession.compactionPending 的写入权\n // (turn-gated 语义),coordinator 只决定\"要不要请求\"。\n requestCompaction: (agentSession, msgCount) =>\n this.requestEscapeValveCompaction(agentSession, msgCount),\n maxHistoryMessages: SESSION_MAX_HISTORY_MESSAGES,\n inlineCheckInterval: CAP_INLINE_CHECK_INTERVAL,\n levelDivisor: LEVEL_DIVISOR,\n })\n this.panelStateRestorer = new PanelStateRestorer({\n ...(deps.panelStateStore ? { panelStateStore: deps.panelStateStore } : {}),\n logger: deps.logger,\n })\n this.crashGapDetector = new CrashGapDetector({\n traceStore: deps.traceStore,\n logger: deps.logger,\n // RFC-328 D4:本类是 crash-gap 的唯一发布 owner——detector 只交还事实,\n // 事件形状与发布时机(检出即发,供运行时消费者/审计面)由此处决定。\n onDetected: (agentSession, gap) => {\n agentSession.publish({\n 'session.crash-gap-detected': [\n {\n type: 'session.crash-gap-detected' as const,\n sessionId: agentSession.id,\n lostTurns: gap.lostTurns,\n lastPersistedAt: gap.lastPersistedAt,\n lastTraceAt: gap.lastTraceAt,\n },\n ],\n })\n },\n })\n }\n\n /**\n * RFC-178 三触发一执行器的**执行汇聚点**:条数 cap(触发器 A,含触发器 C 的压力收紧\n * 系数)+ 字节 cap(触发器 B 硬段,同受 C 收紧)。所有裁剪只影响内存视图(DB\n * append-forever),capped 时 publish `session.history-capped` 供前端提示。\n * doSave 与在线评估(D5)共用本方法,保证两条触发路径行为一致。\n */\n private applyResidencyCaps(agentSession: AgentSession): void {\n // RFC-328 D3:决策与执行委派 coordinator;本方法只保留事件发布(发布 owner 唯一)。\n const result = this.residencyCoordinator.evaluate(agentSession)\n if (!result.shouldPublish) return\n\n agentSession.publish({\n 'session.history-capped': [\n {\n type: 'session.history-capped' as const,\n sessionId: agentSession.id,\n removedCount: result.removedCount,\n trigger: result.trigger,\n capValue: result.capValue,\n // R12:有损裁剪(压缩连续失败后放行,裁到了活跃上下文)显式标记。\n ...(result.lossy ? { lossy: true } : {}),\n },\n ],\n })\n }\n\n /**\n * RFC-321 D1-a′ 逃生阀执行体:请求一次强制压缩(fire-and-forget,不阻塞落盘)。\n *\n * **必须节流**:`activeOversized` 在活跃区持续超限期间会**每次 save/在线评估都为真**,\n * 无节流将导致每次落盘都发起一次 LLM 摘要调用(成本与限流灾难)。节流语义与\n * `capPublishBaseline` 同构——自上次请求以来消息数须再涨一个滞回增量才允许再请求;\n * 压缩成功会使活跃区骤降,信号自然消失,故正常情况下每个\"超限期\"只请求一次。\n *\n * 失败不上抛:压缩失败(LLM 异常/无可压缩内容)只记日志,下一个增量窗口会再试;\n * 裁剪结果与落盘均不受影响。\n */\n private requestEscapeValveCompaction(agentSession: AgentSession, msgCount: number): void {\n // 终局 review(2026-08-07 三轮)修复:节流增量原用 SESSION_MAX_HISTORY_MESSAGES 推导\n // (50000×0.1=5000 条)——那是条数闸的滞回尺度。但压缩前置的主要触发方是**字节闸**\n // (64MB),典型 20KB/条形态触发时仅 ~3200 条,会话到 lossy 阈值(3 次失败)前需再涨\n // 15000 条 ≈ 300MB——底线1 早已击穿。字节路径的重试节奏必须按字节尺度推导:\n // 64MB×0.1=6.4MB ÷ 典型 2KB/条 ≈ 3300 条每档太粗,直接取固定 200 条(与在线评估\n // CAP_INLINE_CHECK_INTERVAL 同尺度)——3 次失败窗口内最多再涨 600 条 ≈ 12MB,\n // 内存敞口可控,重试频率(每 200 条一次 LLM 调用尝试)也不构成成本风暴。\n if (!this.residencyCoordinator.shouldRequestCompaction(agentSession.id, msgCount)) return\n\n // RFC-324 D1 M2:逃生阀压缩从 fire-and-forget 升级为 turn-gated——Promise 写回\n // AgentSession.compactionPending,runPromptRound 开头 await 后才继续 prompt。\n // 原 fire-and-forget(void compactNow().then(...))在图片密集会话(token 闸失明)\n // 下不保证收敛:压缩 5s 内可追加 50 条消息,压缩永远跟不上 → 滑向 lossy。\n // 现在 pending 状态由 runPromptRound 的 gate 阻塞下回合,确保压缩完成后才发新 prompt。\n const compactionPromise = agentSession\n .compactNow()\n .then((r) => {\n // R12:压缩\"成功\"以真的产生了边界为准(compacted=true);仅调用不抛错但未压缩\n // (消息太少/切点不可得)同样计失败,否则会永久拒裁。\n if (r.compacted) {\n this.residencyCoordinator.onCompactionSucceeded(agentSession.id)\n } else {\n this.residencyCoordinator.onCompactionFailed(agentSession.id)\n }\n this.deps.logger.info(\n { sessionId: agentSession.id, compacted: r.compacted, before: r.before, after: r.after },\n '[cap] escape-valve compaction requested (RFC-321 R12 compaction-first)',\n )\n return r\n })\n .catch((err) => {\n const failures = this.residencyCoordinator.onCompactionFailed(agentSession.id)\n this.deps.logger.warn(\n { sessionId: agentSession.id, error: err, failures },\n '[cap] escape-valve compaction failed; lossy trim allowed after threshold',\n )\n // 不 rethrow——pending Promise 的 rejection 由 runPromptRound 的 gate 捕获处理,\n // 如果此处 rethrow 会产生 unhandled rejection(runPromptRound 可能还没开始 await)。\n return { compacted: false, before: 0, after: 0 }\n })\n agentSession.compactionPending = compactionPromise\n }\n\n /**\n * RFC-178 D5(收编 RFC-147 E-2):在线 cap 评估——消息条数自上次评估以来增长超过\n * `CAP_INLINE_CHECK_INTERVAL` 时立即执行驻留裁剪,不等 doSave 节奏。挂在 turn.end\n * 订阅(每 turn 一次 O(n) 计数,非每条消息),把\"两次 save 之间无界增长\"收敛到有界。\n */\n private maybeInlineCapEval(agentSession: AgentSession): void {\n if (this.residencyCoordinator.shouldEvaluateInline(agentSession)) {\n this.applyResidencyCaps(agentSession)\n }\n }\n\n /**\n * autoPersist 时订阅 prompt 终态(prompt.end / error)与 session.cleared(clear 后增量落盘清空态)→ 增量落盘。\n * fire-and-forget + 告警日志:落盘失败不阻塞会话主流程,stop() 的 saveAll 仍兜底。\n * transient(orchestrator ephemeral 子会话)不订阅——避免写盘后又 removeSession\n * 删、且被 restoreAll 当一等会话复活。\n *\n * RFC-305 D1(2026-07-31):turn.end 每次入链落 entries 增量(doSaveEntries,无节流、\n * 不含 panelState)——长循环 DB 崩溃窗口收窄到毫秒级,见常量头注释与 RFC-305 §3。\n */\n track(agentSession: AgentSession, transient: boolean): void {\n if (!this.deps.autoPersist || transient) {\n return\n }\n\n // RFC-160 D2a:注入运行时增量剥离的资格谓词——\"前条 compaction 已持久化\"查内存\n // entry_id 游标(save 链成功后才推进),不查 DB(save 异步 fire-and-forget 有假阴性)。\n // 判定 = 目标 entry 在 entries 序中位于游标之前/之上。O(n) 扫描仅在 compaction 时触发\n // (低频,~900 条消息一次)。能力缺席(非 Relational 后端)→ 不注入 = 运行时剥离禁用。\n const persistence = this.deps.persistence as unknown as {\n getPersistedCursor?: (sessionId: string) => string | undefined\n }\n const raw = agentSession.session\n if (raw instanceof InMemorySession && typeof persistence.getPersistedCursor === 'function') {\n raw.setReplacementStripEligibility((entryId: string) => {\n const cursor = persistence.getPersistedCursor!(agentSession.id)\n if (!cursor) {\n this.deps.logger.debug?.(\n { sessionId: agentSession.id, entryId },\n 'RFC-160 D2a: skip runtime replacement strip (no persisted cursor yet)',\n )\n return false\n }\n let seenTarget = false\n for (const e of raw.entries()) {\n if (e.id === entryId) seenTarget = true\n if (e.id === cursor) {\n if (!seenTarget) {\n this.deps.logger.debug?.(\n { sessionId: agentSession.id, entryId, cursor },\n 'RFC-160 D2a: skip runtime replacement strip (entry beyond persisted cursor)',\n )\n }\n return seenTarget\n }\n }\n return false\n })\n }\n\n // RFC-159 D3:写租约被拒是用户必须感知的状态(本进程改动不会落盘),不能只留后台\n // 日志——发结构化事件供前端提示(同 session.history-capped 模式)。识别用 name 而非\n // instanceof:错误可能跨包边界(session dist ↔ runtime src),原型链不可靠。\n //\n // 2026-07-14:denied↔restored 状态转换同时在此维护——`leaseDeniedState` 记录\"本会话\n // 最近一次 save 是否处于只读降级\",仅在真实发生**转换**时各 publish 一次事件(避免\n // 每次 denied/成功都重复告警)。restored 依赖\"下一次 save 成功\"才能感知,若用户在\n // 只读期间不再产生新消息则不会触发——这是已知的悲观延迟,UI 文案需明确告知用户\n // \"下次保存成功时自动恢复\"而非承诺实时探测。\n const notifyIfLeaseDenied = (error: unknown): boolean => {\n if (error instanceof Error && error.name === 'SessionWriteLeaseDeniedError') {\n const wasDenied = this.leaseDeniedState.get(agentSession.id) === true\n this.leaseDeniedState.set(agentSession.id, true)\n if (!wasDenied) {\n agentSession.publish({\n 'session.write-lease-denied': [\n { type: 'session.write-lease-denied' as const, sessionId: agentSession.id },\n ],\n })\n }\n return true\n }\n return false\n }\n\n const notifyIfLeaseRestored = (): void => {\n if (this.leaseDeniedState.get(agentSession.id) === true) {\n this.leaseDeniedState.set(agentSession.id, false)\n agentSession.publish({\n 'session.write-lease-restored': [\n { type: 'session.write-lease-restored' as const, sessionId: agentSession.id },\n ],\n })\n }\n }\n\n // notify* 逻辑必须在 `enqueue()` 追踪的同一条 promise 链内部执行(而非在其外部用\n // `.then()/.catch()` 追加)——否则 `flushPending()`(只 await `saveChains.get(id)`)\n // 会在 notify 回调实际执行前就 resolve,导致外部观察者(如测试、或需要\"落盘+通知\"\n // 一并完成再继续的调用方)看到不一致的中间态。用 `this.enqueue()` 直接包一个内部\n // async job(而非调用公开的 `save()` 再外挂 `.then`),使其与 `saveChains` 完全同源。\n const persist = () => {\n void this.enqueue(agentSession.id, async () => {\n try {\n await this.doSave(agentSession.id)\n notifyIfLeaseRestored()\n } catch (error) {\n if (!notifyIfLeaseDenied(error)) {\n this.deps.logger.warn(\n {\n sessionId: agentSession.id,\n err: error instanceof Error ? error.message : String(error),\n },\n 'Session auto-persist failed',\n )\n }\n }\n })\n }\n\n // RFC-305 M3 分流:'incremental' → 每 turn 入链落 entries 增量(无节流、不 await、\n // 不含 panelState,长循环崩溃窗口 = 单条入链在途毫秒级,论证见文件头);\n // 'snapshot' → 30s/5turn 节流(后端整份上传能力边界),prompt.end 仍强制落库。\n const flushTurnEntries = () => {\n const id = agentSession.id\n void this.enqueue(id, async () => {\n try {\n await this.doSaveEntries(id)\n notifyIfLeaseRestored()\n } catch (error) {\n if (!notifyIfLeaseDenied(error)) {\n this.deps.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'Session turn-level auto-persist failed',\n )\n }\n }\n })\n }\n\n const persistOnTurnEnd = () => {\n const id = agentSession.id\n // RFC-178 D5:在线 cap 评估先于落盘——驻留裁剪不被 save 节奏拖延\n // (收编 RFC-147 E-2\"两次 save 之间无界增长\")。\n this.maybeInlineCapEval(agentSession)\n\n const mode = this.deps.turnFlush ?? 'incremental'\n if (mode === 'snapshot') {\n const now = Date.now()\n const state = this.turnSaveState.get(id)\n if (!state) {\n this.turnSaveState.set(id, { lastSaveAt: now, turnsSinceSave: 1 })\n return\n }\n state.turnsSinceSave++\n const elapsed = now - state.lastSaveAt\n if (state.turnsSinceSave < TURN_SAVE_THROTTLE_COUNT && elapsed < TURN_SAVE_THROTTLE_MS) {\n return\n }\n state.lastSaveAt = now\n state.turnsSinceSave = 0\n }\n flushTurnEntries()\n }\n\n // RFC-194 D2:clear 落盘链专用闭包——顺序 = 边界落树(已发生) → doSave 成功 →\n // purgeClearedHistory 物理释放内存驻留。先 save 后 purge 是数据安全红线(RFC-159\n // append-forever):落盘异步节流下 clear 时边界前可能有未落盘消息,先 purge 会让\n // 它们永远进不了 DB。save 失败(含写租约被拒)→ 跳过 purge(宁可多驻留不可丢数据),\n // 驻留交 RFC-178 三触发兜底;不做\"下次 save 成功补 purge\"状态机(clear 可重触发)。\n const persistThenPurge = () => {\n void this.enqueue(agentSession.id, async () => {\n try {\n await this.doSave(agentSession.id)\n notifyIfLeaseRestored()\n const raw = agentSession.session\n if (raw instanceof InMemorySession) {\n const purged = raw.purgeClearedHistory()\n if (purged.removedCount > 0 || purged.strippedReplacements > 0) {\n this.deps.logger.debug?.(\n { sessionId: agentSession.id, ...purged },\n 'RFC-194: purged cleared history after successful save',\n )\n }\n }\n } catch (error) {\n if (!notifyIfLeaseDenied(error)) {\n this.deps.logger.warn(\n {\n sessionId: agentSession.id,\n err: error instanceof Error ? error.message : String(error),\n },\n 'Session clear-persist failed (purge skipped, residency left to RFC-178 caps)',\n )\n }\n }\n })\n }\n\n agentSession.subscribe('prompt.end', persist)\n agentSession.subscribe('error', persist)\n agentSession.subscribe('session.cleared', persistThenPurge)\n agentSession.subscribe('turn.end', persistOnTurnEnd)\n\n // RFC-336 D2:输入边界落盘策略注入。放在 track() 内 = 与四条事件订阅同一处,\n // 天然共享其前置条件(本方法开头已 `if (!autoPersist || transient) return`),\n // 不会出现\"订阅没建但落盘回调建了\"的错配。snapshot 后端的豁免在被调方判定。\n //\n // 可选调用:`track()` 的入参在测试与嵌入场景中可能是只实现了订阅面的轻量替身\n // (既有 fake 早于本能力存在)。缺该方法 = 不做输入边界落盘,退回改动前行为,\n // 与其他可选能力(getPersistedCursor、removeLast)的 fail-open 惯例一致。\n if (agentSession.setInputBoundaryFlusher) {\n agentSession.setInputBoundaryFlusher(() =>\n this.requestInputBoundaryFlush(agentSession, transient),\n )\n } else {\n // 可观测性(审核建议 P3):生产路径的 AgentSession 一定有此方法(class 公开方法),\n // 走到这里意味着传入的是轻量替身——输入边界落盘静默失效。记 debug 便于排障,\n // 不升级为 warn(测试替身是合法用法,不是错误)。\n this.deps.logger.debug?.(\n { sessionId: agentSession.id },\n 'RFC-336 D2: input-boundary flush disabled (session lacks setInputBoundaryFlusher)',\n )\n }\n }\n\n async flushPending(id: string): Promise<void> {\n const pendingSave = this.saveChains.get(id)\n if (pendingSave) {\n // 终局 review(2026-07-21):此前 `.catch(() => {})` 静默吞掉全部落盘失败(含写租约\n // 被拒、磁盘满、权限错误)——调用方(removeSession/restore)对失败零感知,日志层面\n // 也无任何痕迹可查。enqueue 链内部的 job(persist/persistOnTurnEnd 等)已各自 warn\n // 一次,这里的 catch 本只是防止 flushPending 自身向上抛错中断调用方流程;但双重\n // try/catch 不冲突——job 内 warn 是\"落盘失败\"事件,这里补一条\"等待中的落盘链最终\n // 以失败告终\"的确认日志,帮助运维在 removeSession 场景下判断是否有数据风险。\n await pendingSave.catch((error: unknown) => {\n this.deps.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'flushPending: pending save chain settled with failure (already logged by the job itself; surfaced here for removeSession/restore callers)',\n )\n })\n }\n // P2 缓解:flushPending 是会话移除路径(removeSession)的必经调用点——顺带清理\n // per-session 状态,避免已删除会话的记录在 Map 里无限期残留(小体量但真实的内存泄漏)。\n this.leaseDeniedState.delete(id)\n this.turnSaveState.delete(id)\n this.remoteRevisions.delete(id)\n }\n\n /**\n * P2 缓解(独立 review 补充,2026-07-12):会话可能经 idle 驱逐(`collectIdle`)或满池 LRU\n * 驱逐(`evictLeastRecent`)消失,这两条路径不经过 `removeSession`/`flushPending`(前者是\n * 同步批量收集不适合 await 落盘完成,后者已自行 fire-and-forget 触发 `saveSnapshot`)——\n * 提供一个轻量同步纯清理方法,供这两条驱逐路径调用,与 `flushPending` 共享同一清理意图\n * 但不附带\"等待落盘完成\"的语义(驱逐场景不需要,也不应阻塞驱逐循环)。\n */\n /**\n * RFC-323 D4:计算用于预算分配的活跃会话数(排除 transient)。\n *\n * transient 会话(orchestrator ephemeral 子会话)不订阅 persistence track、\n * 不被 applyResidencyCaps 管理,但存在于 sessions Map 中(listSessionIds 会返回)。\n * 若计入 N 会摊薄正常会话的预算。故过滤 configs.get(id)?.transient === true。\n */\n private countActiveSessionsForAllocation(): number {\n const ids = this.deps.listSessionIds()\n let n = 0\n for (const id of ids) {\n const config = this.deps.getConfig?.(id)\n if (config?.transient === true) continue\n n++\n }\n return Math.max(1, n)\n }\n\n /**\n * RFC-325 M2:汇总所有活跃会话的总估算字节驻留——供 GlobalResidencyEngine 做压力计算。\n * 只读 estimatedContentBytes 增量计数器(O(会话数),不遍历消息)。\n *\n * 口径:**含被评估的目标会话本身**。pressure 是\"全局已用/总预算\"的比值,\n * 目标会话的驻留同样占用全局预算,排除它会低估压力。与 countActiveSessionsForAllocation\n * 一致地排除 transient(不订阅持久化、不参与预算分摊)。\n */\n private sumEstimatedContentBytes(): number {\n let total = 0\n for (const id of this.deps.listSessionIds()) {\n const config = this.deps.getConfig?.(id)\n if (config?.transient === true) continue\n const session = this.deps.getSession(id)\n if (session?.session instanceof InMemorySession) {\n total += session.session.estimatedContentBytes\n }\n }\n return total\n }\n\n forgetSession(id: string): void {\n this.leaseDeniedState.delete(id)\n this.residencyCoordinator.forget(id)\n this.crashGapDetector.forget(id)\n this.turnSaveState.delete(id)\n this.remoteRevisions.delete(id)\n }\n\n /**\n * RFC-352 D2:驱逐的最终 snapshot 仍在队列中时不能提前清 residency 状态。\n * 若同 id 已被 restore/recreate,则新 session 已取得该 id 的状态所有权,旧驱逐完成\n * 不得反向清掉它。\n */\n forgetEvictedSession(id: string): void {\n if (this.deps.getSession(id)) return\n this.forgetSession(id)\n }\n\n async save(id: string): Promise<void> {\n return this.enqueue(id, () => this.doSave(id))\n }\n\n /**\n * RFC-336 D1:会话行预建——会话创建即落 `sessions` 一行(无 entries),不等首条消息。\n *\n * **要解决的问题**(实测复现,2026-08-10):`repo.createSession()` 的唯一调用点是\n * `RelationalSessionPersistence.save()`,而 save 由 `track()` 订阅的 `turn.end`/\n * `prompt.end` 触发——两者都发生在**首个模型回合完成之后**。因此\"用户已回车、模型还没\n * 回任何东西\"这段窗口(≈ 一次完整模型往返,数秒~数十秒)内进程若被强杀(SIGKILL/OOM/\n * 断电),DB 里连 `sessions` 行都不存在 → 整个会话蒸发,`/resume` 完全看不到,\n * 且 `detectCrashGap` 因 `load()` 返回 null 而根本不执行(RFC-305 D3 的盲区)。\n *\n * **为什么复用 doSaveEntries 而非直调 repo**(D1-b,RFC-159 D3):写租约检查在\n * `RelationalSessionPersistence.save()` 入口,是所有会话写路径的单一汇聚点。直调\n * `repo.createSession()` 会开一条无租约的写路径先例。走这里则天然经 `enqueue` 串行链\n * → `doSaveEntries` → `persistence.save()`,租约与写序语义全部继承,零新增写路径。\n * 空 entries 快照在 save 内部即 `createSession()` + 零次 `appendEntry()`,正是所需语义。\n *\n * **幂等**:`createSession` 是 `ON CONFLICT DO NOTHING`;重复调用安全。\n * **fire-and-forget**(规则 3):失败只 warn,绝不阻塞会话创建——预建是可用性增强,\n * 不是创建流程的正确性前提(失败时退化为改动前行为:首个 turn 后才落行)。\n */\n /**\n * RFC-336 D2:输入边界落盘——prep 成功、模型往返开始**之前**把新增 entries 落库。\n *\n * 要解决的问题:落库此前只由 `turn.end`/`prompt.end` 触发(RFC-305 D1),两者都在首个\n * 模型回合完成之后。M1 的会话行预建保证了\"会话不蒸发\",但那条 user message 本身仍要\n * 等首个 turn 才进 DB——强杀窗口 ≈ 一次完整模型往返(数秒~数十秒)。本方法把该窗口\n * 压到\"单条入链在途(毫秒级)\",与 RFC-305 对 turn 2+ 的保证对齐。\n *\n * **T336-8 后端能力分流**(与 `persistOnTurnEnd` 同源判据 `deps.turnFlush`):\n * - `'incremental'`(本地 relational SQLite,缺省):游标增量 append,O(新增条数),\n * WAL 单事务毫秒级 → 每次输入边界都落,拿满收益。\n * - `'snapshot'`(远端 PUT / 旧 blob 逃生口):每次落库都是**整份快照上传/覆写**,\n * 随会话线性变贵。为一条 user message 付全量上传代价不划算,且远端本就有\n * 30s/5turn 节流的既定策略(RFC-305 M3)→ **豁免**,维持 `prompt.end` 强制落库\n * 兜底。这不是能力缺失,是后端能力边界下的正确取舍(RFC-336 §D2 约束)。\n *\n * fire-and-forget(规则 3)+ 复用 `enqueue` 串行链(规则 2/9):与预建、turn 落盘、\n * prompt 终态落盘共用同一条 per-session 链,天然串行、写租约语义一致。\n */\n requestInputBoundaryFlush(agentSession: AgentSession, transient: boolean): void {\n if (!this.deps.autoPersist || transient) return\n // snapshot 后端豁免:整份上传代价 ≫ 单条消息价值,prompt.end 仍强制落库兜底。\n if ((this.deps.turnFlush ?? 'incremental') === 'snapshot') return\n\n const id = agentSession.id\n void this.enqueue(id, async () => {\n try {\n await this.doSaveEntries(id)\n } catch (error) {\n // 写租约被拒 = 本进程对该会话只读,属预期内良性情况(同 precreateSessionRow)。\n if (error instanceof Error && error.name === 'SessionWriteLeaseDeniedError') return\n this.deps.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'Input-boundary flush failed (RFC-336 D2; falls back to turn/prompt-end persistence)',\n )\n }\n })\n }\n\n precreateSessionRow(agentSession: AgentSession, transient: boolean): void {\n // transient(orchestrator ephemeral 子会话)契约上就不落盘,与 track() 的跳过条件一致;\n // autoPersist=false(in-memory 后端)同理——不向内存持久化写无意义的空快照。\n if (!this.deps.autoPersist || transient) return\n\n const id = agentSession.id\n void this.enqueue(id, async () => {\n try {\n await this.doSaveEntries(id)\n } catch (error) {\n // 写租约被拒(另一进程持有)在此路径是**预期内**的良性情况,不升级为告警噪音:\n // 本进程对该会话只读,预建本就不该发生。其余错误记 warn 供排障。\n if (error instanceof Error && error.name === 'SessionWriteLeaseDeniedError') return\n this.deps.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'Session row pre-creation failed (RFC-336 D1; falls back to first-turn persistence)',\n )\n }\n })\n }\n\n /**\n * 驱逐路径专用:用调用方**同步捕获**的快照入链落盘。\n * 会话即将出池,doSave 经 getSession 会扑空;同链串行保证不被在途旧写覆盖。\n *\n * 终局 review 复核(2026-07-21):曾怀疑\"若同一会话有更早入链、尚未完成的 doSave job,\n * 本方法用调用方在驱逐时刻捕获的(可能更旧的)metadata 覆盖 doSave 写入的更新值\"——\n * 追踪 enqueue() 的调用序保证后确认**不成立**,理由:\n * ① 调用方(session-manager 的 collectIdle/evictLeastRecent)捕获快照与紧随其后的\n * `agentSession.dispose()` 之间没有任何 await/yield 点,JS 单线程保证这段同步区间\n * 会话状态不可能再变化——本方法拿到的 snapshot 是驱逐那一刻真正最新的状态;\n * ② `enqueue()` 严格按调用顺序串行执行同一 session 的 job 链——若有更早的 doSave job\n * 仍在途(尚未执行到位),它必然排在本方法的 job **之前**执行,不可能反过来;\n * ③ `dispose()` 会 unsub 掉 `prompt.end`/`turn.end` 订阅,故此后不会再有新 doSave 入链;\n * ④ 即便某个更早的 doSave job 直到 dispose 之后才真正开始执行,届时 `getSession(id)`\n * 已因会话被 `sessions.delete(id)` 而扑空,`doSave` 提前 return(no-op),不写入\n * 任何数据——不存在\"新值被旧值覆盖\"的路径。\n * 结论:本方法在当前单进程同步模型下天然是该会话 job 链的最后一环,写入的是最新状态,\n * 无需额外的新鲜度校验。跨进程并发写由写租约(RFC-159 D3)独立兜底,与本方法无关。\n */\n saveSnapshot(\n id: string,\n snapshot: Omit<SessionSnapshot<AgentMessage>, 'version' | 'id'>,\n onSettled?: () => void,\n ): Promise<void> {\n const metadata = this.mergeSnapshotConfig(id, snapshot.metadata)\n const save = this.enqueue(id, async () => {\n const fullSnapshot: SessionSnapshot<AgentMessage> = {\n version: SESSION_SNAPSHOT_VERSION,\n id,\n ...snapshot,\n metadata,\n }\n // RFC-352 D3:eviction is also a remote write path. It must share the same\n // revision-aware save as live persistence, never fall back to an unconditional PUT.\n if (this.isRemoteCasPersistence()) {\n await this.saveRemoteWithCas(id, fullSnapshot)\n } else {\n await this.deps.persistence.save(fullSnapshot)\n }\n\n // RFC-108 D3:面板态独立持久化——驱逐前最后一次落盘,尝试从仍活着的 session 读取面板态。\n // 若 session 已出池(getSession 扑空)则跳过——doSave 在更早的 auto-persist 中已保存。\n if (this.deps.panelStateStore) {\n const rawSession = this.deps.getSession(id)?.session\n if (rawSession) {\n await this.deps.panelStateStore.saveAll(id, {\n sessionId: id,\n todoList: rawSession.getTodoList(),\n editedFiles: rawSession.getEditedFiles(),\n subagents: rawSession.getSubagents(),\n drafts: rawSession.getDrafts(),\n turnSummaries: rawSession.getTurnSummaries(),\n })\n }\n }\n })\n if (onSettled) {\n void save.then(onSettled, onSettled)\n }\n return save\n }\n\n /**\n * 合并快照 metadata.config——外部解析配置(getConfig→pickPersistableConfig)\n * 与快照内已有的 inputHistory 叠加。两条落盘路径(saveSnapshot/doSave)共用,\n * 避免合并逻辑 split-brain。无任何来源时原样返回 metadata。\n *\n * RFC-108 D3:todoList/editedFiles/subagents/drafts 四个面板字段已从 metadata.config\n * 分离至 PanelStateStore,不再在此合并——落盘时由 doSave/saveSnapshot 额外调用\n * panelStateStore.saveAll() 独立持久化。\n *\n * inputHistory(@deprecated RFC-088 M4b)保留现有迁移路径不变。\n */\n private mergeSnapshotConfig(\n rawId: string,\n snapshotMetadata: SessionSnapshot<AgentMessage>['metadata'],\n ): SessionSnapshot<AgentMessage>['metadata'] {\n const extConfig = this.deps.getConfig?.(rawId)\n const persistedConfig = extConfig ? pickPersistableConfig(extConfig) : undefined\n const historyFromSnapshot = snapshotMetadata.config?.inputHistory\n const mergedConfig =\n persistedConfig || historyFromSnapshot\n ? {\n ...persistedConfig,\n ...(historyFromSnapshot ? { inputHistory: historyFromSnapshot } : {}),\n }\n : undefined\n return mergedConfig ? { ...snapshotMetadata, config: mergedConfig } : snapshotMetadata\n }\n\n private enqueue(id: string, job: () => Promise<void>): Promise<void> {\n const prev = this.saveChains.get(id) ?? Promise.resolve()\n const next = prev.catch(() => {}).then(job)\n this.saveChains.set(id, next)\n const cleanup = () => {\n if (this.saveChains.get(id) === next) {\n this.saveChains.delete(id)\n }\n }\n void next.then(cleanup, cleanup)\n return next\n }\n\n /**\n * RFC-305 D2:entries 档——residency cap 评估 + 快照增量 append(游标式,O(新消息))。\n * turn.end 高频路径走此档;panelState(每次全量重写 5 个存储)不在其中,避免每 turn\n * 纯写放大。residency cap 保留在两档共同前缀(RFC-178 D5 在线评估语义不变)。\n */\n private async doSaveEntries(id: string): Promise<void> {\n const agentSession = this.deps.getSession(id)\n if (!agentSession) {\n return\n }\n\n const rawSession = agentSession.session\n if (rawSession instanceof InMemorySession) {\n // RFC-178:三触发一执行器汇聚(条数 A + 字节 B 硬段 + 压力收紧 C 系数),\n // capped 时 publish session.history-capped(可观测性沿革见 applyResidencyCaps\n // 注释与 2026-07-12 修复/RFC-159 T6 语义澄清:裁剪仅内存视图,DB append-forever)。\n this.applyResidencyCaps(agentSession)\n const snapshot = rawSession.toSnapshot()\n const metadata = this.mergeSnapshotConfig(rawSession.id, snapshot.metadata)\n const fullSnapshot: SessionSnapshot<AgentMessage> = {\n version: SESSION_SNAPSHOT_VERSION,\n id: rawSession.id,\n ...snapshot,\n metadata,\n }\n\n // M1 远端权威:远端后端走 CAS(If-Match + 409 reload/rebase/重试一次)。\n // 本地后端(Relational/File)走原路径——它们有自有租约/append-only 保护。\n if (this.isRemoteCasPersistence()) {\n await this.saveRemoteWithCas(id, fullSnapshot)\n } else {\n await this.deps.persistence.save(fullSnapshot)\n }\n }\n }\n\n /** M1:duck-type 检测远端 CAS 能力——避免硬 import RemoteSessionPersistence(保持 runtime 不反向依赖 session 具体类)。 */\n private isRemoteCasPersistence(): boolean {\n return typeof (this.deps.persistence as RemoteCasPersistence).saveWithRevision === 'function'\n }\n\n /**\n * M1 远端权威写入:CAS + 409 自动 reload/rebase/单次重试。\n *\n * 策略:\n * 1. 首次 PUT 带本进程已知的 `expectedRevision`(首次写入时 undefined = 无条件建)。\n * 2. 409 冲突 → 从服务端 load 最新快照 + revision。\n * 3. rebase:以服务端 entries 为基线,追加本进程内存中比基线更新的 entry(按 entry_id 去重)。\n * 4. 用新 revision 重 PUT 一次。再失败则抛出(不递归重试,防死循环)。\n *\n * rebase 语义对齐本地 append-only 模型:不删除服务端已有 entry,只追加本进程的新尾。\n * 这与 RelationalSessionPersistence 的\"cursor 增量 append\"同构——区别仅在远端是整份\n * PUT 而非 per-entry INSERT,所以 rebase 后仍需整份上传(直到远端支持 append-tail API)。\n */\n private async saveRemoteWithCas(\n id: string,\n localSnapshot: SessionSnapshot<AgentMessage>,\n ): Promise<void> {\n const casPersistence = this.deps.persistence as RemoteCasPersistence\n const expected = this.remoteRevisions.get(id)\n\n try {\n const newRevision = await casPersistence.saveWithRevision(localSnapshot, expected)\n this.remoteRevisions.set(id, newRevision)\n } catch (error) {\n if (!(error instanceof RemoteSnapshotConflictError)) {\n throw error\n }\n\n // 409 冲突 → reload 服务端最新 + rebase\n this.deps.logger.warn(\n { sessionId: id, expectedRevision: expected, serverRevision: error.info.revision },\n 'Remote session CAS conflict; reloading + rebasing for one retry',\n )\n\n const remote = await casPersistence.loadWithRevision(id)\n if (!remote) {\n // 服务端快照已被删除——用本地作新基线,expectedRevision=0(期望新建)\n const newRevision = await casPersistence.saveWithRevision(localSnapshot, 0)\n this.remoteRevisions.set(id, newRevision)\n return\n }\n\n const rebased = this.rebaseSnapshotAgainstRemote(localSnapshot, remote.snapshot)\n const newRevision = await casPersistence.saveWithRevision(rebased, remote.revision)\n this.remoteRevisions.set(id, newRevision)\n }\n }\n\n /**\n * M1 rebase:把本地新尾追加到服务端快照之后。\n *\n * 按 entry_id 去重——服务端已有的 entry 保留,本地比服务端新的 entry 追加到尾部。\n * 不删除任何服务端 entry(append-only 红线在远端同样适用)。\n * metadata 取本地版本(本地有最新的 lastActiveAt/config 等可变字段)。\n */\n private rebaseSnapshotAgainstRemote(\n local: SessionSnapshot<AgentMessage>,\n remote: SessionSnapshot<AgentMessage>,\n ): SessionSnapshot<AgentMessage> {\n const remoteEntryIds = new Set(remote.entries.map((e) => e.id))\n const localOnly = local.entries.filter((e) => !remoteEntryIds.has(e.id))\n\n return {\n version: SESSION_SNAPSHOT_VERSION,\n id: local.id,\n entries: [...remote.entries, ...localOnly],\n metadata: local.metadata,\n leafId: local.leafId ?? remote.leafId,\n }\n }\n\n /** RFC-305 D2:完整档 = entries + panelState。prompt 终态/clear/驱逐/saveAll 走此档。 */\n private async doSave(id: string): Promise<void> {\n await this.doSaveEntries(id)\n\n const agentSession = this.deps.getSession(id)\n if (!agentSession) {\n return\n }\n const rawSession = agentSession.session\n\n // RFC-108 D3:面板态独立持久化——与主快照同批保存,保持相同的 await 等待语义。\n if (this.deps.panelStateStore) {\n await this.deps.panelStateStore.saveAll(id, {\n sessionId: id,\n todoList: rawSession.getTodoList(),\n editedFiles: rawSession.getEditedFiles(),\n subagents: rawSession.getSubagents(),\n drafts: rawSession.getDrafts(),\n turnSummaries: rawSession.getTurnSummaries(),\n })\n }\n }\n\n async saveAll(): Promise<void> {\n // 真空会话(从未被任何显式动作——prompt/草稿/todo 等——碰过,entries 恒为空)跳过\n // 落盘:与 collectIdle/evictLeastRecent 两条驱逐路径的既有守卫(`if (snapshot.entries\n // .length > 0)`)保持一致。此前 saveAll()(`/exit` 时的自动批量落盘)对每个\"打开即\n // 退出、从未产生任何状态\"的会话都执行一次 `INSERT INTO sessions`(ON CONFLICT DO\n // NOTHING 也会先插入一行),在数据库里永久留下垃圾并污染 `/resume` 列表——真空会话\n // 没有任何可丢失的内容,跳过是零风险的。\n //\n // 注意:只在 saveAll()(批量自动落盘)加此守卫,不下沉进 doSave()/save() 本身——\n // 后者是显式调用点(如草稿保存 onDraftSave、测试里的手动 `sessions.save(id)`),\n // 其语义是\"用户/调用方明确要求现在落盘\",即便消息列表为空也可能有值得保存的\n // 会话级配置/草稿/todoList(session-restore-lifecycle.test.ts H7 等用例已覆盖\n // 这类\"配置持久化但从未 prompt\"场景,下沉会破坏其显式落盘语义)。\n const ids = [...this.deps.listSessionIds()].filter((id) => {\n const session = this.deps.getSession(id)?.session\n return !(session instanceof InMemorySession) || session.entries().length > 0\n })\n const results = await Promise.allSettled(ids.map((id) => this.save(id)))\n results.forEach((r, i) => {\n if (r.status === 'rejected') {\n this.deps.logger.warn(\n {\n sessionId: ids[i],\n err: r.reason instanceof Error ? r.reason.message : String(r.reason),\n },\n 'saveAll: a session failed to persist (others unaffected)',\n )\n }\n })\n await Promise.allSettled([...this.saveChains.values()])\n }\n\n /**\n * 从持久化存储恢复单个 session。\n * 若未提供 configProvider 则返回 null。\n */\n async restore(id: string): Promise<AgentSession | null> {\n if (!this.deps.configProvider) {\n return null\n }\n\n const existing = this.deps.getSession(id)\n if (existing) {\n return existing\n }\n\n await this.flushPending(id)\n\n const loaded = this.isRemoteCasPersistence()\n ? await (this.deps.persistence as RemoteCasPersistence).loadWithRevision(id)\n : null\n const snapshot = loaded?.snapshot ?? (loaded === null && this.isRemoteCasPersistence()\n ? null\n : await this.deps.persistence.load(id))\n if (!snapshot) {\n return null\n }\n\n if (snapshot.version !== SESSION_SNAPSHOT_VERSION) {\n this.deps.logger.warn(\n { sessionId: id, version: snapshot.version, expected: SESSION_SNAPSHOT_VERSION },\n 'Session snapshot version mismatch, skipped',\n )\n return null\n }\n\n if (!this.deps.canAccommodate(id)) {\n throw new Error(`Max sessions reached, cannot restore ${id}.`)\n }\n\n const rawSession = new InMemorySession<AgentMessage>(snapshot.id)\n rawSession.restoreEntries(\n snapshot.entries,\n { ...snapshot.metadata, lastActiveAt: Date.now() },\n snapshot.leafId,\n )\n\n const config = applyPersistedConfig(\n this.deps.configProvider(\n snapshot.metadata.config?.agentName,\n snapshot.metadata.config?.modelId,\n ),\n snapshot.metadata.config,\n )\n const agentSession = await this.deps.adoptRestoredSession(rawSession, config)\n\n // 归档态回放:新建 Agent 状态机默认 idle,与持久化镜像(metadata.archived)无关联——\n // 若快照标记为已归档,显式回放一次 archive() 使运行时状态机与持久化状态重新一致\n // (idle→archived 转换合法,见 VALID_TRANSITIONS)。跳过 setArchived 的 lastActiveAt\n // 触碰:archive() 内部会再次调用 session.setArchived(true),与快照值一致,幂等安全。\n if (snapshot.metadata.archived) {\n agentSession.archive()\n }\n\n // RFC-108 D3/D4:面板态恢复 + 一次性迁移\n await this.panelStateRestorer.restore(rawSession, snapshot, id)\n\n // RFC-352 D3:seed the first post-restore CAS write with the revision that\n // produced this snapshot. Without this, undefined means unconditional PUT.\n if (loaded) this.remoteRevisions.set(id, loaded.revision)\n this.track(agentSession, false)\n\n // RFC-305 D3:trace↔DB 崩溃对账(fail-soft,可见性增强非正确性门)。\n // 放在 track() 之后:publish 的事件需要订阅面就绪;放在 return 前:restore 语义不变。\n await this.crashGapDetector.detect(agentSession, snapshot)\n\n this.deps.logger.info({ sessionId: id }, 'Session restored')\n return agentSession\n }\n\n /**\n * RFC-305 D3:restore 时 trace↔DB 对账——检出\"上次进程异常终止时已发生但未落库\"的\n * turn 缺口并告警。对账基准 = trace append-log(逐事件实时落盘,独立于 save 链):\n * DB 最后一条已持久化 entry 时间戳之后,trace 仍有 `turn.end` lifecycle 事件 →\n * 这些 turn 的消息只进过内存、从未进 DB,已随崩溃永久丢失。\n *\n * 用时间戳而非 turn 号对账:trace 的 turn 字段 per-prompt 从 0 重置,跨 prompt 不可比;\n * ts 与 entry.timestamp 同源(Date.now 系时钟),单机单调可比。\n *\n * 容忍度:正常退出路径(prompt.end 立即落盘 + turn 级入链)下 trace turn.end 与 DB\n * 落库几乎同时,容忍 CRASH_GAP_TOLERANCE_MS 内的尾差(在途写 + 时钟粒度),超出才告警。\n *\n * fail-soft:traceStore 缺失/读取异常/无 turn.end 事件 → 静默跳过(debug 日志),\n * 绝不阻塞 restore、绝不产生假告警。\n */\n\n /**\n * RFC-305 D3:对账结果查询口——restore/restoreAll 内部把本次恢复检出的崩溃缺口记入\n * per-session 缓存,调用方(CLI resume 路径)在订阅建立后读取并 toast。缓存随\n * forgetSession 清理,避免陈旧告警在后续 restore 重复弹出。\n */\n consumeCrashGap(id: string): { lostTurns: number } | undefined {\n // 值通道(评审 C1):restore/restoreAll 均早于订阅者 attach,纯 publish 无人消费,\n // 故 CLI toast 必须走本返回值。事件通道在检出时已由 detector 的 onDetected 回调发出。\n const gap = this.crashGapDetector.consume(id)\n return gap ? { lostTurns: gap.lostTurns } : undefined\n }\n\n\n /**\n * 从持久化存储批量恢复 session:最近活跃优先,最多填到剩余容量。\n * 装不下的更旧快照留在盘上(restore(id) 可按需复活),不挤掉在池会话。\n * 若未提供 configProvider 则直接返回 0。\n */\n async restoreAll(): Promise<number> {\n if (!this.deps.configProvider) {\n return 0\n }\n\n const room = this.deps.capacityLeft()\n if (room === 0) {\n const ids = await this.deps.persistence.list()\n const pending = ids.filter((id) => !this.deps.hasSession(id)).length\n if (pending > 0) {\n this.deps.logger.info(\n { candidates: pending },\n 'Session pool full, restoreAll skipped (restore on demand)',\n )\n }\n return 0\n }\n\n // RFC-154 D1:两阶段化——阶段一走 listRecentMeta 轻量拿到已排序(lastActiveAt 降序)+\n // 已排除归档会话的候选 id(不做 entries 反序列化),阶段二只对这些候选做重量级 load。\n // candidateIds 已按 lastActiveAt 降序排列,调用方信任该顺序、不再做二次 sort() 兜底\n // (契约见 SessionPersistence.listRecentMeta 文档注释)。\n const candidateIds = await this.deps.persistence.listRecentMeta(room)\n\n const admitted: Array<{ snapshot: SessionSnapshot<AgentMessage>; revision?: number }> = []\n let skipped = 0\n\n for (const id of candidateIds) {\n if (this.deps.hasSession(id)) {\n continue\n }\n\n let loaded: { snapshot: SessionSnapshot<AgentMessage>; revision?: number } | null\n try {\n if (this.isRemoteCasPersistence()) {\n const remote = await (this.deps.persistence as RemoteCasPersistence).loadWithRevision(id)\n loaded = remote ? { snapshot: remote.snapshot, revision: remote.revision } : null\n } else {\n const snapshot = await this.deps.persistence.load(id)\n loaded = snapshot ? { snapshot } : null\n }\n } catch (error) {\n this.deps.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'restoreAll: a session snapshot failed to load (skipped, others unaffected)',\n )\n continue\n }\n if (!loaded) {\n continue\n }\n const snapshot = loaded.snapshot\n\n if (snapshot.version !== SESSION_SNAPSHOT_VERSION) {\n this.deps.logger.warn(\n { sessionId: id, version: snapshot.version, expected: SESSION_SNAPSHOT_VERSION },\n 'Session snapshot version mismatch, skipped',\n )\n continue\n }\n\n admitted.push(loaded)\n }\n\n // 并发防御:restoreAll 逐条 load 期间,池容量可能被并发 createSession 挤占(既有测试\n // \"restoreAll 并发安全\"覆盖此场景)——candidateIds 已是排序后的定长候选集,不会超过\n // room,但 liveRoom 可能小于 room(并发挤占),此处仍需按最新容量截断,保序(已按\n // lastActiveAt 降序排列,从尾部截断即保留最近活跃者)。\n const liveRoom = this.deps.capacityLeft()\n while (admitted.length > liveRoom) {\n admitted.pop()\n skipped++\n }\n\n if (skipped > 0) {\n this.deps.logger.info(\n { skipped, capacity: liveRoom },\n 'Sessions exceeding pool capacity left on storage (restore on demand)',\n )\n }\n\n let restored = 0\n let latestId: string | null = null\n // RFC-154 D1:admitted 现按 lastActiveAt 降序排列(listRecentMeta 契约,最新在前)——\n // 与旧实现(admitted 升序、迭代到最后一条即最新)方向相反。latestId 改为只在\"首次成功\n // 恢复\"时赋值一次:由于顺序保证降序,第一个成功恢复的快照就是候选集里最近活跃者,\n // 后续更旧的不应覆盖它(旧实现是\"迭代到最后覆盖\",本质也是\"取最近活跃者\",语义等价)。\n for (const admittedSnapshot of admitted) {\n const { snapshot } = admittedSnapshot\n if (this.deps.hasSession(snapshot.id)) {\n continue\n }\n\n try {\n const rawSession = new InMemorySession<AgentMessage>(snapshot.id)\n rawSession.restoreEntries(\n snapshot.entries,\n { ...snapshot.metadata, lastActiveAt: Date.now() },\n snapshot.leafId,\n )\n\n const config = applyPersistedConfig(\n this.deps.configProvider(\n snapshot.metadata.config?.agentName,\n snapshot.metadata.config?.modelId,\n ),\n snapshot.metadata.config,\n )\n const agentSession = await this.deps.adoptRestoredSession(rawSession, config)\n\n // 归档态回放(同 restore() 单条路径,见其注释)。\n if (snapshot.metadata.archived) {\n agentSession.archive()\n }\n\n // RFC-108 D3/D4:面板态恢复 + 一次性迁移\n await this.panelStateRestorer.restore(rawSession, snapshot, snapshot.id)\n\n if (admittedSnapshot.revision !== undefined) {\n this.remoteRevisions.set(snapshot.id, admittedSnapshot.revision)\n }\n this.track(agentSession, false)\n\n // RFC-328 M1:restoreAll 此前**遗漏** crash-gap 对账(只有 restore(id) 单条路径有),\n // 导致批量恢复的会话即使有崩溃缺口也永远不告警。两入口现走同一 detector,\n // 且同样在 track() 之后调用以保持订阅时序。fail-soft 语义由 detector 保证。\n await this.crashGapDetector.detect(agentSession, snapshot)\n\n restored++\n if (latestId === null) {\n latestId = snapshot.id\n }\n } catch (error) {\n this.deps.logger.warn(\n { sessionId: snapshot.id, err: error instanceof Error ? error.message : String(error) },\n 'restoreAll: 单条会话重建失败(跳过,余者不受影响)',\n )\n }\n }\n\n if (latestId) {\n this.deps.setActiveIfNone(latestId)\n }\n\n this.deps.logger.info({ count: restored }, 'Sessions restored')\n return restored\n }\n}\n\n/**\n * M1 远端 CAS 能力面(duck-type):RemoteSessionPersistence 实现了这两个方法,\n * runtime 不硬 import 具体类,只用结构类型检测。本地后端没有这两个方法 → 走原路径。\n */\ninterface RemoteCasPersistence extends SessionPersistence<AgentMessage> {\n saveWithRevision(snapshot: SessionSnapshot<AgentMessage>, expectedRevision?: number): Promise<number>\n loadWithRevision(id: string): Promise<{ snapshot: SessionSnapshot<AgentMessage>; revision: number } | null>\n}\n","import {\n InMemorySession,\n InMemorySessionPersistence,\n FileSessionPersistence,\n RemoteSessionPersistence,\n hasStrippedReplacements,\n supportsReplacementHydration,\n} from '@x-otto/session'\nimport type { LeaseInspection } from '@x-otto/session'\nimport { SDK_VERSION } from '@x-otto/env'\nimport { SESSION_MAX, SESSION_IDLE_TIMEOUT_MS } from '@x-otto/session'\nimport { createStreamFunction, type ProviderRegistry } from '@x-otto/ai'\nimport {\n createTraceRecorder,\n createRecordingPorts,\n systemClock,\n systemRandom,\n systemId,\n} from '@x-otto/agent'\nimport type {\n AgentMessage,\n ClockPort,\n StreamFunction,\n TraceEvent,\n TraceRecorder,\n} from '@x-otto/agent'\nimport type { AppendLog } from '@x-otto/persistence'\nimport { type HookRegistry } from '@x-otto/hooks'\nimport { type Logger } from '@x-otto/shared'\n\nimport { AgentSession } from './agent-session'\nimport { TimeTravelController } from './time-travel-controller'\nimport { PersistenceSync } from './persistence-sync'\nimport { buildForkedSession } from './fork'\nimport { resolveSessionTitle } from './derive-title'\nimport type { PanelStateStore } from './panel-state-store'\nimport { LEVEL_DIVISOR, type ResidencyGovernor } from './residency-governor'\nimport { decide as decideResidency } from './global-residency-engine'\nimport { resolveResidencyBudget } from '@x-otto/session-contract'\n\nimport type { Session, SessionPersistence } from '@x-otto/session'\nimport type { ReplacementHydrationCapable, SessionEntry } from '@x-otto/session'\nimport type { Devtools } from '@x-otto/devtools'\nimport type { AgentTool } from '@x-otto/agent'\nimport type { MemoryPort } from './memory-transform'\nimport type { Checkpoint, CheckpointData, RestoreMode } from './checkpoint'\nimport type { ReplayLevel, SandboxRunner } from './sandbox'\nimport type { TracebackView } from './traceback'\nimport type { AgentSessionInfo, ResolvedSessionConfig } from './types'\n\n/**\n * Manager 只持有基础设施配置(持久化、网络、日志)。\n * 业务配置(model、systemPrompt、tools 等)由宿主(coding/第二宿主)解析后\n * 经 ResolvedSessionConfig 传入 createSession,Manager 不做二次 merge。\n */\nexport interface ResidencyDiagnostics {\n activeSessionCount: number\n totalResidentBytes: number\n globalPressure: number\n isGlobalPressure: boolean\n maxBytesPerSession: number\n pressureLevel: 'healthy' | 'warning' | 'critical'\n}\n\nexport interface SessionPoolOptions {\n hookRegistry: HookRegistry\n providerRegistry: ProviderRegistry\n maxSessions?: number\n idleTimeoutMs?: number\n threshold?: number\n logger: Logger\n devtools?: Devtools\n /**\n * 自动上下文管理器。注入后由引擎 pipeline 的 context-transform 阶段驱动\n * needsPrune/needsCompaction→process。app 级基础设施,跨所有 session 共享。\n */\n memory?: MemoryPort\n /**\n * trace 落盘 store。app 级共享,per-session 以 sessionId 分流。注入后 manager 为每个\n * session 构造绑定 sessionId 的 TraceRecorder 并透传引擎;未注入即不录制。\n */\n traceStore?: AppendLog<TraceEvent>\n /**\n * checkpoint 落盘 store。app 级共享,per-session 以 sessionId 分流(append-only,seq=checkpoint 序号)。\n * 注入后 createCheckpoint/restoreCheckpoint 可用;未注入则相关方法抛错。\n */\n checkpointStore?: AppendLog<CheckpointData>\n /**\n * 确定性时钟端口。缺省=真实系统时钟(行为不变);replay 由 trace 回灌。\n */\n clock?: ClockPort\n /**\n * 仅用于 restore / restoreAll 场景。\n * 正常 createSession 路径必须传入完整 ResolvedSessionConfig,不使用此字段。\n * 使用 factory function 而非静态快照,确保每次 restore 拿到最新配置。\n * 可接 agentName,restore 据此重解析子代理 prompt 身份(缺省=main)。\n */\n configProvider?: (agentName?: string, modelId?: string) => ResolvedSessionConfig\n /**\n * RFC-108 D3:面板态(todoList/editedFiles/subagents/drafts)独立持久化门店。\n * 未注入时优雅降级为不独立持久化——面板态仅存在于内存,重启后丢失但不报错崩溃。\n */\n panelStateStore?: PanelStateStore\n /**\n * RFC-178 触发器 C:驻留收紧系数源(可选)。组装层把 MemoryGovernor 分级信号写入\n * 其 setLevel;未注入时 cap 恒用全值(行为与此前一致)。透传给 PersistenceSync。\n */\n residencyGovernor?: ResidencyGovernor\n /** RFC-323 M4:驻留预算运行时配置 getter(settings 注入,透传给 PersistenceSync)。 */\n getResidencyConfig?: () => import('@x-otto/session-contract').ResidencyBudgetConfig | undefined\n /**\n * RFC-146:本地高效冷会话枚举策略(可选注入)。仅本地 SQLite 后端在\n * `session-pool-factory.ts` 构造期注入(内部持有 `SqliteSessionRepository`,\n * 单次 SQL 查询即拿到完整 `AgentSessionInfo[]`,远快于\"逐个 HTTP load()\");\n * 远端后端(`otto serve` / `remote-persistence-server`)不注入,缺省走\n * `listAllPersistedSessions` 的 `persistence.listPaginated()+load()` 兜底路径。\n * `SessionPool` 只判断\"策略是否已注入\",不感知底层是什么仓储实现——\n * 消灭对具体仓储类型的依赖(见 RFC-146 §6b「sessionRepoRef 迁移策略」)。\n */\n listColdSessionsOverride?: (workspaceKey: string) => Promise<AgentSessionInfo[]>\n /**\n * 只读探测(可选注入):某会话当前是否被**另一存活进程**持有写租约(RFC-159 D3\n * SessionWriteLeaseManager.peekLockedByOther 的薄封装,无副作用)。仅本地 SQLite 后端\n * 注入(`session-pool-factory.ts`);未注入时 `listSessions`/`listAllPersistedSessions`\n * 恒不标注只读(远端/内存后端无租约机制,行为与迁移前一致)。用于 `/resume` 列表\n * 提前展示\"切进去会是只读\",而非等到真正 save 被拒才事后感知。\n */\n isSessionWriteLocked?: (sessionId: string) => boolean\n /**\n * RFC-171 D4:写租约只读诊断(可选注入)——`SessionWriteLeaseManager.inspect` 的薄封装。\n * 仅本地 SQLite 后端注入。供 `/session unlock` 只读诊断模式消费(持有者 pid/hostname/\n * 存活性/mtime 距今);未注入时恒返回 `{ locked: false }`(远端/内存后端无租约机制)。\n */\n inspectSessionWriteLease?: (sessionId: string) => LeaseInspection\n /**\n * RFC-171 D4:强制释放写租约(可选注入)——`SessionWriteLeaseManager.forceRelease` 的\n * 薄封装。跳过 token 实核,仅供用户主动触发的 `/session unlock --force` 使用。\n */\n forceReleaseSessionWriteLease?: (sessionId: string) => void\n}\n\nexport interface DiskSessionPoolOptions extends SessionPoolOptions {\n sessionDir: string\n}\n\nexport interface RemoteSessionPoolOptions extends SessionPoolOptions {\n sessionUrl: string\n /** fetch 实现(必填)。在浏览器环境可传 globalThis.fetch,Node.js 需传兼容实现。 */\n fetch: typeof globalThis.fetch\n /** 返回认证 token 的工厂函数(必填)。 */\n getAuth: () => Promise<{ token: string }>\n timeoutMs?: number\n /** user identity id,非空时请求带 `X-Otto-User-Id` header。 */\n userId?: string\n /** workspace 分区键(缺省 'default')——修复(N3):remote 后端此前恒 'default',多本地\n * workspace 共享同一 --session-url 时互相冲突,见 RemoteSessionPersistenceOptions.wsKey。\n * 支持惰性 getter(App 构造期 workspaceRef 尚未解析完成)。 */\n wsKey?: string | (() => string | undefined)\n}\n\nexport interface SessionPorts {\n clock?: ClockPort\n recorder?: TraceRecorder\n /** RFC-145 D3:nondet batch 冲洗回调(RecordingPorts.flushNondetBatch)。缺省 = 不聚合。 */\n nondetFlush?: () => void\n stream?: StreamFunction\n tools?: AgentTool[]\n disableMemory?: boolean\n}\n\nexport class SessionPool {\n /** RFC-338 D7:面板态门店引用(供 panelState getter 暴露给 cli)。 */\n private readonly panelStateStoreRef: PanelStateStore | undefined\n\n private readonly sessions = new Map<string, AgentSession>()\n private readonly configs = new Map<string, ResolvedSessionConfig>()\n\n private readonly persistence: SessionPersistence<AgentMessage>\n\n private readonly maxSessions: number\n private readonly idleTimeoutMs: number\n private readonly threshold: number\n\n private activeSessionId?: string\n\n public readonly stream: StreamFunction\n private readonly hookRegistry: HookRegistry\n private readonly logger: Logger\n private readonly devtools?: Devtools\n private readonly memory?: MemoryPort\n private readonly traceStore?: AppendLog<TraceEvent>\n private readonly checkpointStore?: AppendLog<CheckpointData>\n private readonly clock?: ClockPort\n private readonly residencyGovernor?: ResidencyGovernor\n private readonly getResidencyConfig?: () => import('@x-otto/session-contract').ResidencyBudgetConfig | undefined\n\n private readonly configProvider?: (agentName?: string, modelId?: string) => ResolvedSessionConfig\n /** RFC-146:本地高效冷会话枚举策略(可选,见 SessionPoolOptions.listColdSessionsOverride)。 */\n private readonly listColdSessionsOverride?: (workspaceKey: string) => Promise<AgentSessionInfo[]>\n /** 只读探测(可选,见 SessionPoolOptions.isSessionWriteLocked)。 */\n private readonly isSessionWriteLocked?: (sessionId: string) => boolean\n /** RFC-171 D4:写租约诊断/强制释放(可选,见 SessionPoolOptions 对应字段)。 */\n private readonly inspectSessionWriteLeaseFn?: (sessionId: string) => LeaseInspection\n private readonly forceReleaseSessionWriteLeaseFn?: (sessionId: string) => void\n\n /**\n * 持久化是否为「自动行为」。disk/remote 后端 = true(prompt 终态增量落盘 +\n * 宿主 start/stop 自动 restoreAll/saveAll);in-memory 缺省 = false(行为与历史一致,\n * 不向 in-memory persistence 写无意义快照)。遵循\"写入是默认行为非手动 API\"\n * 的原则(会话持续落盘 + --continue 恢复)。\n */\n public readonly autoPersist: boolean\n\n /** 时间旅行编排控制器(checkpoint/rollback/fork/replay/traceback 的实现归属)。 */\n public readonly timeTravel: TimeTravelController\n /** 快照持久化同步(增量落盘 + 重启恢复,第二变更轴)。 */\n private readonly persistenceSync: PersistenceSync\n\n constructor(\n options: SessionPoolOptions,\n persistence: SessionPersistence<AgentMessage>,\n autoPersist = false,\n ) {\n this.autoPersist = autoPersist\n this.stream = createStreamFunction(options.providerRegistry)\n this.hookRegistry = options.hookRegistry\n this.logger = options.logger\n this.devtools = options.devtools\n this.memory = options.memory\n this.traceStore = options.traceStore\n this.checkpointStore = options.checkpointStore\n this.clock = options.clock\n this.residencyGovernor = options.residencyGovernor\n this.getResidencyConfig = options.getResidencyConfig\n this.configProvider = options.configProvider\n this.listColdSessionsOverride = options.listColdSessionsOverride\n this.isSessionWriteLocked = options.isSessionWriteLocked\n this.inspectSessionWriteLeaseFn = options.inspectSessionWriteLease\n this.forceReleaseSessionWriteLeaseFn = options.forceReleaseSessionWriteLease\n this.persistence = persistence\n\n const resolvedMax = options.maxSessions ?? SESSION_MAX\n this.maxSessions = resolvedMax\n this.idleTimeoutMs = options.idleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS\n this.threshold = Math.max(0, Math.min(options.threshold ?? resolvedMax, resolvedMax))\n\n this.timeTravel = new TimeTravelController({\n logger: this.logger,\n traceStore: this.traceStore,\n checkpointStore: this.checkpointStore,\n getSession: (id) => this.sessions.get(id),\n getConfig: (id) => this.configs.get(id),\n createSession: (config, ports) => this.createSession(config, ports),\n ensureCapacity: (id) => {\n if (this.sessions.has(id)) {\n throw new Error(`Session with ID ${id} already exists.`)\n }\n this.prepareCapacity(id)\n },\n adoptForkedSession: (raw, config) => this.adoptForkedSession(raw, config),\n // RFC-160 D3:持久层 hydrate 能力(Relational 后端实现;file/memory/remote 不剥离\n // 故 hasStrippedReplacements 恒 false,出口快路径直通,此依赖不会被调用)。\n ...(supportsReplacementHydration<AgentMessage>(this.persistence)\n ? {\n hydrateEntries: (sessionId: string, entries: readonly SessionEntry<AgentMessage>[]) =>\n (this.persistence as unknown as ReplacementHydrationCapable<AgentMessage>)\n .hydrateReplacements(sessionId, entries),\n }\n : {}),\n })\n\n this.persistenceSync = new PersistenceSync({\n persistence: this.persistence,\n logger: this.logger,\n autoPersist: this.autoPersist,\n // RFC-305 D3:trace↔DB 崩溃对账数据源(缺省 = 对账跳过,restore 行为不变)。\n traceStore: this.traceStore,\n // RFC-305 M3:turn-flush 能力由持久化后端声明(incremental 每 turn / snapshot 节流)。\n turnFlush: this.persistence.turnFlush,\n getSession: (id) => this.sessions.get(id),\n getConfig: (id) => this.configs.get(id),\n hasSession: (id) => this.sessions.has(id),\n listSessionIds: () => this.sessions.keys(),\n canAccommodate: (id) => this.canAccommodate(id),\n capacityLeft: () => Math.max(0, this.maxSessions - this.sessions.size),\n adoptRestoredSession: async (raw, config) => {\n const agentSession = this.buildAgentSession(raw, config)\n this.sessions.set(raw.id, agentSession)\n this.configs.set(raw.id, config)\n await this.hookRegistry.emit('session.restored', { sessionId: raw.id, metadata: {} })\n agentSession.notifyCreated()\n return agentSession\n },\n setActiveIfNone: (id) => {\n if (!this.activeSessionId) {\n this.activeSessionId = id\n }\n },\n configProvider: this.configProvider,\n panelStateStore: options.panelStateStore,\n residencyGovernor: options.residencyGovernor,\n getResidencyConfig: options.getResidencyConfig,\n })\n // RFC-338 D7:留一份引用供 `panelState` getter 暴露给 cli(paste 缓存读写)。\n this.panelStateStoreRef = options.panelStateStore\n }\n\n /**\n * RFC-338 D7:面板态门店的只读暴露。\n *\n * cli 侧需要它来读写 paste 缓存(`getPaste`/`setPaste`)——粘贴缓存的加载/落盘\n * 时机由 UI 事件驱动(启动、切会话、fork、提交后),不在 PersistenceSync 的\n * 回合级落盘节奏上,故不能只走 persistence-sync 内部通路。\n * 只读 getter:调用方不得替换门店实例。未注入后端时为 undefined(优雅降级)。\n */\n get panelState(): PanelStateStore | undefined {\n return this.panelStateStoreRef\n }\n\n get active(): AgentSession | undefined {\n return this.activeSessionId ? this.sessions.get(this.activeSessionId) : undefined\n }\n\n /**\n * 会话级「始终允许」工具名读写(宿主桥接 PermissionRegistry↔持久化用)。写入会话当前\n * ResolvedSessionConfig(= getConfig 同一引用),随 autoPersist 落盘、restore 经 applyPersistedConfig\n * 回灌;未知会话为 no-op / 空数组。空集合写入即清除该字段。\n */\n getSessionPermissionAllow(id: string): string[] {\n return [...(this.configs.get(id)?.permissionAlwaysAllow ?? [])]\n }\n\n setSessionPermissionAllow(id: string, tools: readonly string[]): void {\n const config = this.configs.get(id)\n if (!config) return\n if (tools.length === 0) {\n delete config.permissionAlwaysAllow\n return\n }\n config.permissionAlwaysAllow = [...tools]\n }\n\n /** 会话级「账号可用模型」读写(同 permissionAlwaysAllow 持久化语义)。 */\n getSessionAvailableModels(id: string): string[] {\n return [...(this.configs.get(id)?.availableModels ?? [])]\n }\n\n setSessionAvailableModels(id: string, models: readonly string[]): void {\n const config = this.configs.get(id)\n if (!config) return\n if (models.length === 0) {\n delete config.availableModels\n return\n }\n config.availableModels = [...models]\n }\n\n /**\n * 保持既有 private 外观(createSession/fork/restore 调用点不变)。\n */\n private trackPersistence(agentSession: AgentSession, transient: boolean): void {\n this.persistenceSync.track(agentSession, transient)\n }\n\n /**\n * RFC-336 D1/D1-c:新会话(createSession / fork adopt)落盘行预建。\n *\n * 与 `trackPersistence` 成对出现——track 订阅\"此后的变更\",本方法保证\"此刻的存在性\"。\n * 两条新建路径都需要:`adoptForkedSession` 此前同样只 track 不写 DB,fork 出的会话在\n * 首个事件前被强杀同样蒸发(D1-c)。restore 路径**不需要**——那条路径的会话按定义\n * 已在 DB 中。\n */\n private precreateSessionRow(agentSession: AgentSession, transient: boolean): void {\n this.persistenceSync.precreateSessionRow(agentSession, transient)\n }\n\n /**\n * 两路 fork——`forkSession`(当前 leaf 全量)与 `forkFromCheckpoint`(历史 checkpoint 点)——\n * 共用的会话采纳:登记 + 持久化追踪 + `session.created` hook + `notifyCreated`。使 fork 出的会话\n * **既一等可观测**(service/桥接据 created 挂 trace/event)**又落盘持久**(重启经 restoreAll 恢复)。\n * 不改 active(fork 不抢用户当前会话,与历史行为一致);transient=false(fork 是一等持久分支,落盘)。\n */\n private async adoptForkedSession(\n raw: InMemorySession<AgentMessage>,\n config: ResolvedSessionConfig,\n ): Promise<AgentSession> {\n const agentSession = this.buildAgentSession(raw, config)\n this.sessions.set(raw.id, agentSession)\n this.configs.set(raw.id, config)\n this.trackPersistence(agentSession, false)\n // RFC-336 D1-c:fork 出的会话与 createSession 同等预建——此前只 track 不写 DB,\n // fork 在首个事件前被强杀同样蒸发(fork 携带 source entries,故这里落的不是空行)。\n this.precreateSessionRow(agentSession, false)\n await this.hookRegistry.emit('session.created', { sessionId: raw.id, metadata: {} })\n agentSession.notifyCreated()\n return agentSession\n }\n\n private buildAgentSession(\n rawSession: Session<AgentMessage>,\n config: ResolvedSessionConfig,\n ports?: SessionPorts,\n ): AgentSession {\n const resolved = ports ?? this.recordPorts(rawSession.id)\n return new AgentSession(rawSession, {\n // RFC-324 D1/R5:turn-gate 超时兜底惰性读(settings 在 App 构造后才 load)。\n // 未配置时 resolveResidencyBudget 回退 SESSION_COMPACTION_TIMEOUT_MS(含 env)。\n getCompactionTimeoutMs: () => resolveResidencyBudget(this.getResidencyConfig?.()).compactionTimeoutMs,\n model: config.model,\n titleModel: config.titleModel,\n modelResolution: config.modelResolution,\n agentName: config.agentName,\n depth: config.depth,\n interactive: config.interactive,\n retryAccelerate: config.retryAccelerate,\n streamRetry: config.streamRetry,\n networkDisconnectRetry: config.networkDisconnectRetry,\n systemPrompt: config.systemPrompt,\n systemTail: config.systemTail,\n tools: resolved.tools ?? config.tools,\n thinkingLevel: config.thinkingLevel,\n maxToolTurns: config.maxToolTurns,\n maxToolTurnExtensions: config.maxToolTurnExtensions,\n promptOutputTokenBudget: config.promptOutputTokenBudget,\n promptWallClockBudgetMs: config.promptWallClockBudgetMs,\n stallDetection: config.stallDetection,\n sessionCostBudgetUSD: config.sessionCostBudgetUSD,\n todoContinuation: config.todoContinuation,\n promptRefresh: config.promptRefresh,\n workspaceDir: config.workspaceDir,\n // 修复:此前漏传 workspaceId——即便 ResolvedSessionConfig.workspaceId 已正确解析,\n // AgentSession 构造参数也从未接收到它,导致 AgentSession.workspaceId 恒 undefined\n // (根因链的最后一环:resolver 算对了、buildAgentSession 组装漏了)。\n workspaceId: config.workspaceId,\n hookRegistry: this.hookRegistry,\n stream: resolved.stream ?? this.stream,\n logger: this.logger.child({ sessionId: rawSession.id }),\n devtools: this.devtools,\n memory: resolved.disableMemory ? undefined : this.memory,\n recorder: resolved.recorder,\n nondetFlush: resolved.nondetFlush,\n clock: resolved.clock,\n describeImages: config.describeImages,\n })\n }\n\n private recordPorts(sessionId: string): SessionPorts {\n const store = this.traceStore\n if (!store) {\n return { clock: this.clock }\n }\n const baseClock: ClockPort = this.clock ?? systemClock\n const recorder = createTraceRecorder({\n // 必须把 append() 的 Promise 原样返回给 createTraceRecorder(不能 `void store.append(...)`\n // 丢弃)——它内部靠 `maybe instanceof Promise` 检测并接 .catch(onError);voided 掉的话\n // 该 catch 永远摸不到真实 Promise,reject(如 teardown 期 HttpAppendLog.close 拒绝在途\n // buffer)就变成孤儿 unhandled rejection(RFC-102 M3 验收时在 m13-storage-host-e2e 暴露)。\n // `.then(() => undefined)` 满足 TraceSink.append 的 `Promise<void>` 签名,同时保留链条。\n sink: { append: (event) => store.append(sessionId, event).then(() => undefined) },\n sessionId,\n clock: baseClock,\n })\n const recording = createRecordingPorts(\n { clock: baseClock, random: systemRandom, id: systemId },\n recorder,\n )\n // RFC-145 D3:nondetFlush 随 ports 下传——work-loop 在回合边界与退出路径冲洗 batch。\n return { clock: recording.clock, recorder, nondetFlush: recording.flushNondetBatch }\n }\n\n /**\n * 使用由宿主完整解析好的配置创建 session。\n * Manager 层不再进行任何业务默认值填充。\n */\n async createSession(\n config: ResolvedSessionConfig & { id?: string },\n ports?: SessionPorts,\n ): Promise<AgentSession> {\n const id = config.id ?? crypto.randomUUID()\n\n if (this.sessions.has(id)) {\n throw new Error(`Session with ID ${id} already exists.`)\n }\n\n this.prepareCapacity(id)\n\n const rawSession = new InMemorySession<AgentMessage>(id)\n rawSession.setSdkVersion(SDK_VERSION)\n const agentSession = this.buildAgentSession(rawSession, config, ports)\n\n this.sessions.set(id, agentSession)\n this.configs.set(id, config)\n if (!(config.transient ?? false)) {\n this.activeSessionId = id\n }\n this.trackPersistence(agentSession, config.transient ?? false)\n // RFC-336 D1:立刻预建 DB 行(fire-and-forget,不阻塞创建)——不等首个 turn.end,\n // 否则\"回车后、模型响应前\"被强杀会导致整个会话在 DB 中不存在。\n this.precreateSessionRow(agentSession, config.transient ?? false)\n\n await this.hookRegistry.emit('session.created', {\n sessionId: id,\n metadata: {},\n })\n\n agentSession.notifyCreated()\n\n this.logger.info({ sessionId: id }, 'Session created')\n\n return agentSession\n }\n\n replaySession(\n sourceSessionId: string,\n newId?: string,\n opts: { level?: ReplayLevel; sandbox?: SandboxRunner } = {},\n ): Promise<AgentSession> {\n return this.timeTravel.replaySession(sourceSessionId, newId, opts)\n }\n\n createCheckpoint(\n sessionId: string,\n opts: { label?: string; files?: string[] } = {},\n ): Promise<Checkpoint> {\n return this.timeTravel.createCheckpoint(sessionId, opts)\n }\n\n listCheckpoints(sessionId: string): Promise<Checkpoint[]> {\n return this.timeTravel.listCheckpoints(sessionId)\n }\n\n restoreCheckpoint(sessionId: string, seq: number, mode: RestoreMode = 'both'): Promise<void> {\n return this.timeTravel.restoreCheckpoint(sessionId, seq, mode)\n }\n\n rollbackToCheckpoint(sessionId: string, seq: number, mode: RestoreMode = 'both'): Promise<void> {\n return this.timeTravel.rollbackToCheckpoint(sessionId, seq, mode)\n }\n\n forkFromCheckpoint(sessionId: string, seq: number, newId?: string): Promise<AgentSession> {\n return this.timeTravel.forkFromCheckpoint(sessionId, seq, newId)\n }\n\n replayTrace(sessionId: string, toSeq?: number, fromSeq = 0): Promise<TraceEvent[]> {\n return this.timeTravel.replayTrace(sessionId, toSeq, fromSeq)\n }\n\n traceback(sessionId: string, toSeq: number): Promise<TracebackView> {\n return this.timeTravel.traceback(sessionId, toSeq)\n }\n\n getSession(id: string): AgentSession | undefined {\n return this.sessions.get(id)\n }\n\n /** RFC-325 M5:仅暴露聚合驻留状态,供受控 doctor attach 读取;不泄露会话内容。 */\n getResidencyDiagnostics(): ResidencyDiagnostics {\n let activeSessionCount = 0\n let totalResidentBytes = 0\n for (const [id, agentSession] of this.sessions) {\n if (this.configs.get(id)?.transient) continue\n const raw = agentSession.session\n if (!(raw instanceof InMemorySession)) continue\n // 与 PersistenceSync.countActiveSessionsForAllocation 同口径:在池且非 transient\n // 即参与预算分摊;空历史会话贡献 0 bytes,但不能从 N 中消失。\n activeSessionCount++\n totalResidentBytes += raw.estimatedContentBytes\n }\n const pressureLevel = this.residencyGovernor?.currentLevel ?? 'healthy'\n const decision = decideResidency({\n activeSessionCount,\n currentTotalResidentBytes: totalResidentBytes,\n pressureDivisor: LEVEL_DIVISOR[pressureLevel],\n config: this.getResidencyConfig?.(),\n })\n return {\n activeSessionCount,\n totalResidentBytes,\n globalPressure: decision.globalPressure,\n isGlobalPressure: decision.isGlobalPressure,\n maxBytesPerSession: decision.maxBytesPerSession,\n pressureLevel,\n }\n }\n\n listSessions(filter?: { workspaceId?: string }): AgentSessionInfo[] {\n let sessions = Array.from(this.sessions.values())\n if (filter?.workspaceId) {\n sessions = sessions.filter((s) => s.workspaceId === filter.workspaceId)\n }\n const results = sessions.map((agentSession) => {\n const meta = agentSession.session.metadata()\n return {\n id: agentSession.id,\n title: resolveSessionTitle(\n meta.title,\n agentSession.session.messages(),\n agentSession.id,\n ),\n messageCount: agentSession.session.messages().length,\n createdAt: meta.createdAt ? new Date(meta.createdAt) : new Date(),\n updatedAt: meta.lastActiveAt ? new Date(meta.lastActiveAt) : undefined,\n model: agentSession.model.id,\n status: agentSession.status,\n sdkVersion: meta.sdkVersion,\n workspaceId: agentSession.workspaceId,\n pinned: agentSession.pinned,\n pinGroup: meta.pinGroup,\n writeLocked: this.isSessionWriteLocked?.(agentSession.id) ?? false,\n }\n })\n results.sort((a, b) => {\n const aTime = a.updatedAt?.getTime() ?? a.createdAt.getTime()\n const bTime = b.updatedAt?.getTime() ?? b.createdAt.getTime()\n return bTime - aTime\n })\n return results\n }\n\n /**\n * RFC-146:枚举\"持久化后端已知、但未必已加载进内存\"的会话(本地磁盘冷会话 /\n * 远端历史会话)——`listSessions()` 的超集,供 `/resume` 一类需要看到完整历史的\n * 场景使用。统一返回 `AgentSessionInfo[]`,调用方不感知底层是本地 SQLite 直连\n * 还是远端 HTTP 分页拉取。\n *\n * 两条实现路径(见 RFC-146 §6b「sessionRepoRef 迁移策略」,只统一契约不统一实现):\n * - `listColdSessionsOverride` 已注入(本地场景):直接委托,内部走高效 SQL 查询。\n * - 未注入(远端场景,含 otto serve 与 remote-persistence-server 两种):走\n * `persistence.listPaginated()` 取 id 列表(天然限流,不新发明 cap 机制)+\n * 逐个 `load()` 补 title/createdAt(远端会话缺 messages(),取 metadata.title\n * 回落 id 前 8 位,与本地冷会话降级展示模式一致)。\n *\n * 失败语义(两种失败互不掩盖,见 RFC-146 §6b):\n * - `listPaginated()` 整体失败 → 返回空数组 + warn(批量不可用,非单条问题)。\n * - 单条 `load()` 失败 → 跳过该条、不中止其余(数据损坏/网络抖动等局部问题)。\n *\n * @param filter.workspaceKey 已转换好的分区键(`ws_<id>` 或 `'default'`)——身份→\n * 分区键的转换由调用方(App)负责,本方法不感知 `workspaceId`、不依赖 `@x-otto/workspace`。\n */\n async listAllPersistedSessions(filter?: { workspaceKey?: string }): Promise<AgentSessionInfo[]> {\n const workspaceKey = filter?.workspaceKey ?? 'default'\n\n if (this.listColdSessionsOverride) {\n return this.listColdSessionsOverride(workspaceKey)\n }\n\n let page: { items: string[] }\n try {\n const result = await this.persistence.listPaginated({\n page: 0,\n pageSize: 100,\n sortBy: 'lastActiveAt',\n order: 'desc',\n })\n page = result\n } catch (error) {\n this.logger.warn(\n { workspaceKey, err: error instanceof Error ? error.message : String(error) },\n 'listAllPersistedSessions: listPaginated failed (batch unavailable, not a per-session issue)',\n )\n return []\n }\n\n const results: AgentSessionInfo[] = []\n for (const id of page.items) {\n try {\n const snapshot = await this.persistence.load(id)\n if (!snapshot) continue\n const meta = snapshot.metadata\n results.push({\n id,\n title: meta.title?.trim() || id.slice(0, 8),\n messageCount: meta.messageCount ?? 0,\n createdAt: meta.createdAt ? new Date(meta.createdAt) : new Date(),\n updatedAt: meta.lastActiveAt ? new Date(meta.lastActiveAt) : undefined,\n // /archive /pin 持久化镜像透传(未加载进内存的冷会话同样需要正确展示归档/置顶态,\n // 否则 /resume 列表会把已归档会话误显示为可输入、丢失置顶排序权重/分组)。\n status: meta.archived ? 'archived' : 'idle',\n pinned: meta.pinned,\n pinGroup: meta.pinGroup,\n writeLocked: this.isSessionWriteLocked?.(id) ?? false,\n })\n } catch (error) {\n this.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'listAllPersistedSessions: a session snapshot failed to load (skipped, others unaffected)',\n )\n }\n }\n return results\n }\n\n /**\n * RFC-078 M135-01:回血标题——对所有已加载会话中 title 为空/derived 的,fire-and-forget\n * 触发 ai 重生(跳过 custom)。便宜模型 + 各会话自带 abort/超时,非阻塞。返回触发计数。\n * 冷会话(未 restore 进内存)经 listSessions/resume 读取时已懒解析为 derived(resolveSessionTitle)。\n */\n backfillTitles(): { triggered: number; skipped: number } {\n let triggered = 0\n let skipped = 0\n for (const agentSession of this.sessions.values()) {\n const source = agentSession.session.metadata().titleSource\n if (source === 'custom' || source === 'ai') {\n skipped++\n continue\n }\n agentSession.backfillTitle()\n triggered++\n }\n return { triggered, skipped }\n }\n\n // RFC-325 M4:archive-cold 接线——不只标状态,也要把 DB entries 写入 .jsonl.gz 归档文件。\n // repo.archiveSession 存在已久(RFC-324 D4 M4)但此前**从未接线**;\n // 此外 session-manager.archiveSession 原本只调 agentSession.archive()(标 archived 内存态),\n // 不落盘到归档文件——数据仍在 DB 上无法释放给 LRU。\n async archiveSession(id: string): Promise<boolean> {\n const session = this.sessions.get(id)\n if (!session) return false\n // 先把当前内存快照排入并等待同 session 的持久化串行链。turn.end 的自动增量落盘\n // 是 fire-and-forget;只 flushPending 只能等“已入链”的工作,无法确保归档瞬间的完整快照。\n // `save(id)` 会在同一链上最后写当前状态,故 archive 文件不会遗漏刚完成 turn 的 entries。\n // 任意写失败都在状态切换前抛出,避免“UI 已归档、归档文件不完整”的半状态。\n await this.persistenceSync.save(id)\n\n // DB 侧归档(entries → .jsonl.gz + archived=1),不删除 DB 行(RFC-159 红线)。\n // 只有 relational SQLite 后端实现此 optional 能力。\n if (typeof this.persistence.archiveSession === 'function') {\n await this.persistence.archiveSession(id)\n }\n session.archive()\n return true\n }\n\n async removeSession(id: string): Promise<boolean> {\n const agentSession = this.sessions.get(id)\n if (!agentSession) {\n await this.persistenceSync.flushPending(id)\n const deleted = await this.persistence.delete(id)\n // 幽灵分支同样清 trace/checkpoint——不管快照存不存在,落盘的 trace 可能存在。\n await this.clearSessionStores(id)\n return deleted\n }\n\n agentSession.dispose()\n this.sessions.delete(id)\n this.configs.delete(id)\n\n if (this.activeSessionId === id) {\n this.activeSessionId = undefined\n }\n\n await this.persistenceSync.flushPending(id)\n await this.persistence.delete(id)\n await this.clearSessionStores(id)\n\n // RFC-143 M0:改为 fire-and-forget,与 idle 驱逐(:624)/LRU 驱逐(:683) 语义统一——\n // 会话资源释放(dispose/持久化删除/store 清理)在此之前已完成,session.deleted 是\n // 「通知已在途」的旁路广播,不应让挂起的插件 hook 阻塞用户的显式删除操作。\n // HookRegistry.emit() 自身已有超时兜底(5s),此处的 void 是防止调用方(TUI/API handler)\n // 被卡在「hook 未超时前」的窗口。\n void this.hookRegistry.emit('session.deleted', {\n sessionId: id,\n metadata: { reason: 'removed' },\n })\n this.logger.info({ sessionId: id }, 'Session removed')\n\n return true\n }\n\n /**\n * 删除会话的 trace/checkpoint 落盘数据。此前 removeSession 只删 session 快照,\n * trace JSONL(delta 级、单会话可达数百 MB)与 checkpoint 永久残留——\n * ~/.otto/sessions/trace 累积数十 GB 的直接来源。失败仅告警(孤儿文件可后续清理,\n * 不阻塞会话删除本身)。\n */\n private async clearSessionStores(id: string): Promise<void> {\n const clear = async (label: string, store?: { clear(id: string): Promise<void> }) => {\n if (!store) return\n try {\n await store.clear(id)\n } catch (error) {\n this.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n `Failed to clear ${label} on session removal`,\n )\n }\n }\n await clear('trace store', this.traceStore)\n await clear('checkpoint store', this.checkpointStore)\n }\n\n /**\n * fork 直接复用 source session 自身的已解析配置,\n * 消息通过复制 snapshot entries 完整迁移。\n */\n /**\n * RFC-160 D3/D4:出口 hydrate(fork 用)。无剥离态直通;有剥离态且持久层具备能力则回填;\n * 能力缺席 → 断言炸(接线遗漏立即暴露,绝不把剥离态固化为新会话正文)。\n */\n private async ensureEntriesHydrated(\n sessionId: string,\n entries: readonly SessionEntry<AgentMessage>[],\n exit: string,\n ): Promise<readonly SessionEntry<AgentMessage>[]> {\n if (!hasStrippedReplacements(entries)) return entries\n if (!supportsReplacementHydration<AgentMessage>(this.persistence)) {\n throw new Error(\n `${exit}: entries contain stripped compaction replacements but persistence lacks ` +\n 'hydration capability (RFC-160 D4); refusing to fork stripped state',\n )\n }\n return this.persistence.hydrateReplacements(sessionId, entries)\n }\n\n async forkSession(sourceId: string, newId?: string): Promise<AgentSession | undefined> {\n const sourceSession = this.sessions.get(sourceId)\n const sourceConfig = this.configs.get(sourceId)\n if (!sourceSession || !sourceConfig) {\n return undefined\n }\n\n const sourceRaw = sourceSession.session\n if (!(sourceRaw instanceof InMemorySession)) {\n return undefined\n }\n\n const id = newId ?? crypto.randomUUID()\n this.prepareCapacity(id)\n\n const snapshot = sourceRaw.toSnapshot()\n // RFC-160 D3/D4:fork 产物是**新 session_id** 的行——剥离态落库无跨 session 幂等保护,\n // 会把剥离态固化为新会话的权威正文。fork 前 hydrate;能力缺席却有剥离态 → 断言炸。\n const entries = await this.ensureEntriesHydrated(sourceId, snapshot.entries, 'forkSession')\n const forkedRaw = buildForkedSession(\n id,\n sourceId,\n entries,\n snapshot.metadata,\n snapshot.leafId,\n this.clock,\n )\n\n const forkedConfig: ResolvedSessionConfig = {\n model: sourceSession.model,\n modelResolution: sourceConfig.modelResolution,\n agentName: sourceSession.agentName !== 'agent' ? sourceSession.agentName : undefined,\n systemPrompt: sourceSession.systemPrompt,\n systemTail: sourceSession.systemTail,\n tools: sourceSession.tools,\n thinkingLevel: sourceSession.thinkingLevel,\n maxToolTurns: sourceSession.maxToolTurns,\n maxToolTurnExtensions: sourceSession.maxToolTurnExtensions,\n // RFC-104 D3:fork 拷贝预算定制值(M27-01 点名的 parity 陷阱——漏拷会重置回缺省)。\n promptOutputTokenBudget: sourceSession.promptOutputTokenBudget,\n promptWallClockBudgetMs: sourceSession.promptWallClockBudgetMs,\n todoContinuation: sourceConfig.todoContinuation,\n promptRefresh: sourceSession.promptRefresh,\n workspaceDir: sourceSession.workspaceDir,\n // 修复:fork 此前漏拷 workspaceId——fork 出的会话与源会话应同属一个 workspace\n // (否则 fork 出的分支在 /resume --workspace 过滤/持久化元数据里\"丢失\"workspace 归属)。\n workspaceId: sourceSession.workspaceId,\n depth: sourceConfig.depth,\n transient: sourceConfig.transient,\n interactive: sourceConfig.interactive,\n retryAccelerate: sourceConfig.retryAccelerate,\n streamRetry: sourceConfig.streamRetry,\n networkDisconnectRetry: sourceConfig.networkDisconnectRetry,\n permissionAlwaysAllow: sourceConfig.permissionAlwaysAllow,\n availableModels: sourceConfig.availableModels,\n }\n\n const agentSession = await this.adoptForkedSession(forkedRaw, forkedConfig)\n\n this.logger.info({ sessionId: id, sourceId }, 'Session forked')\n return agentSession\n }\n\n save(id: string): Promise<void> {\n return this.persistenceSync.save(id)\n }\n\n saveAll(): Promise<void> {\n return this.persistenceSync.saveAll()\n }\n\n /**\n * RFC-171 D4:写租约只读诊断(无副作用)——持有者 pid/hostname/存活性/mtime 距今,\n * 供 `/session unlock` 只读模式展示。未注入策略(远端/内存后端)恒返回 `{ locked: false }`。\n */\n inspectSessionWriteLease(sessionId: string): LeaseInspection {\n return this.inspectSessionWriteLeaseFn?.(sessionId) ?? { locked: false }\n }\n\n /**\n * RFC-171 D4:强制释放写租约——跳过 token 实核,仅供用户主动触发的 `/session unlock\n * --force` 使用。未注入策略(远端/内存后端)为 no-op(无租约机制,无需处理)。\n */\n forceReleaseSessionWriteLease(sessionId: string): void {\n this.forceReleaseSessionWriteLeaseFn?.(sessionId)\n }\n\n restore(id: string): Promise<AgentSession | null> {\n return this.persistenceSync.restore(id)\n }\n\n /** RFC-305 D3:消费本次恢复检出的崩溃缺口(CLI resume 路径在订阅建立后读取)。 */\n consumeCrashGap(id: string): { lostTurns: number } | undefined {\n return this.persistenceSync.consumeCrashGap(id)\n }\n\n restoreAll(): Promise<number> {\n return this.persistenceSync.restoreAll()\n }\n\n /**\n * 修复(不一致性,2026-07-12 独立 review 后续核实):此前 idle 驱逐只 dispose+delete,\n * 完全不落盘——若最近一次 auto-persist(prompt.end 触发)因瞬时故障失败(DB 抖动,仅\n * `logger.warn` 无重试,见 `track()` persist 闭包的 catch 分支),内存里的未落盘改动会\n * 被静默丢弃。`evictLeastRecent`(LRU 驱逐)早已对称处理这一点(驱逐前同步捕获快照\n * 入 `saveSnapshot` 链),本方法此前遗漏同样的防御,是纯粹的不一致而非有意设计。\n */\n private collectIdle(): string[] {\n const now = Date.now()\n const removed: string[] = []\n\n for (const [id, agentSession] of this.sessions) {\n // RFC-180 D9(独立评审 P0 F1):暂停态会话必须豁免 idle 驱逐——isBusy 在暂停\n // 生效后天然变回 false(workLoop 已 return,promptGate 复位),与真正空闲会话\n // 在 isBusy 视角下完全无法区分。不豁免会导致用户暂停一个长任务后离开终端,\n // 超过 idle timeout 后暂停语义被驱逐打断(虽 autoPersist 不丢数据,但破坏\n // \"暂停可续跑\"的用户体验)。\n if (agentSession.isBusy || agentSession.isPaused) {\n continue\n }\n\n const meta = agentSession.session.metadata()\n if (now - meta.lastActiveAt > this.idleTimeoutMs) {\n const raw = agentSession.session\n let finalSnapshotQueued = false\n if (this.autoPersist && raw instanceof InMemorySession) {\n const snapshot = raw.toSnapshot()\n if (snapshot.entries.length > 0) {\n finalSnapshotQueued = true\n this.persistenceSync.saveSnapshot(id, snapshot, () => {\n this.persistenceSync.forgetEvictedSession(id)\n }).catch((error: unknown) => {\n this.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'Session snapshot flush on idle eviction failed',\n )\n })\n }\n }\n\n agentSession.dispose()\n this.sessions.delete(id)\n this.configs.delete(id)\n // RFC-352 D2:最终 snapshot 的 cap evaluation 仍依赖 coordinator 状态;仅在\n // 保存完成后清理。没有 snapshot 的空会话可立即释放。\n if (!finalSnapshotQueued) this.persistenceSync.forgetSession(id)\n\n if (this.activeSessionId === id) {\n this.activeSessionId = undefined\n }\n\n void this.hookRegistry.emit('session.deleted', {\n sessionId: id,\n metadata: { reason: 'idle' },\n })\n\n removed.push(id)\n }\n }\n\n return removed\n }\n\n /** 会话条目数(含全部分支)。空判定不能用 messages()——它只走当前分支,多分支会话会被误判为空。 */\n private entryCount(agentSession: AgentSession): number {\n const raw = agentSession.session\n // 终局 review(2026-07-21):entries() 返回内部数组引用(只读长度探测不需要 toSnapshot()\n // 的 [...sessionEntries] 全量浅拷贝)——消除驱逐路径的一次性无谓分配。\n return raw instanceof InMemorySession ? raw.entries().length : raw.messages().length\n }\n\n /**\n * 满池兜底:collectIdle 之后仍满池时,按 lastActiveAt LRU 驱逐会话。\n * 不可驱逐:active、busy(prompt 全生命周期,含流式前窗口)、transient(M18-PR-02:\n * 不许落盘,且生命周期归 orchestrator 管,中途 dispose 会打断编排)。\n * autoPersist 后端驱逐前同步捕获快照入 save 链,内存驱逐零数据损失(restore(id) 可复活);\n * in-memory 后端驱逐非空会话=丢数据,只允许驱逐空会话(从未 prompt 过,无可丢内容)。\n */\n private evictLeastRecent(incomingId: string): void {\n const candidates = Array.from(this.sessions.entries())\n .filter(\n ([id, agentSession]) =>\n id !== incomingId &&\n id !== this.activeSessionId &&\n !agentSession.isBusy &&\n // RFC-180 D9(独立评审 P0 F1):同 collectIdle,暂停态会话豁免 LRU 驱逐候选。\n !agentSession.isPaused &&\n !(this.configs.get(id)?.transient ?? false),\n )\n .filter(([, agentSession]) => this.autoPersist || this.entryCount(agentSession) === 0)\n .sort(([, a], [, b]) => a.session.metadata().lastActiveAt - b.session.metadata().lastActiveAt)\n\n for (const [id, agentSession] of candidates) {\n if (this.sessions.size < this.maxSessions) {\n break\n }\n\n const raw = agentSession.session\n let finalSnapshotQueued = false\n if (this.autoPersist && raw instanceof InMemorySession) {\n const snapshot = raw.toSnapshot()\n if (snapshot.entries.length > 0) {\n finalSnapshotQueued = true\n this.persistenceSync.saveSnapshot(id, snapshot, () => {\n this.persistenceSync.forgetEvictedSession(id)\n }).catch((error: unknown) => {\n this.logger.warn(\n { sessionId: id, err: error instanceof Error ? error.message : String(error) },\n 'Session snapshot flush on eviction failed',\n )\n })\n }\n }\n\n agentSession.dispose()\n this.sessions.delete(id)\n this.configs.delete(id)\n if (!finalSnapshotQueued) this.persistenceSync.forgetSession(id)\n\n void this.hookRegistry.emit('session.deleted', {\n sessionId: id,\n metadata: { reason: 'evicted' },\n })\n\n this.logger.info({ sessionId: id }, 'Session evicted (LRU)')\n }\n }\n\n private prepareCapacity(incomingId: string): void {\n if (this.sessions.has(incomingId)) {\n return\n }\n\n if (this.sessions.size + 1 > this.threshold) {\n this.collectIdle()\n }\n\n if (this.sessions.size >= this.maxSessions) {\n this.evictLeastRecent(incomingId)\n }\n\n if (this.sessions.size >= this.maxSessions) {\n throw new Error(`Max sessions (${this.maxSessions}) reached.`)\n }\n }\n\n private canAccommodate(incomingId: string): boolean {\n if (this.sessions.has(incomingId)) {\n return true\n }\n\n if (this.sessions.size + 1 > this.threshold) {\n this.collectIdle()\n }\n\n if (this.sessions.size >= this.maxSessions) {\n this.evictLeastRecent(incomingId)\n }\n\n return this.sessions.size < this.maxSessions\n }\n\n dispose(): void {\n for (const agentSession of this.sessions.values()) {\n agentSession.dispose()\n }\n this.sessions.clear()\n this.configs.clear()\n this.activeSessionId = undefined\n }\n}\n\nexport function createDiskSessionPool(options: DiskSessionPoolOptions): SessionPool {\n const { sessionDir } = options\n if (!sessionDir) {\n throw new Error('sessionDir is required for DiskSessionPool')\n }\n\n const persistence = new FileSessionPersistence<AgentMessage>({\n baseDir: sessionDir,\n })\n return new SessionPool(options, persistence, true)\n}\n\nexport function createRemoteSessionPool(options: RemoteSessionPoolOptions): SessionPool {\n const { sessionUrl } = options\n if (!sessionUrl) {\n throw new Error('sessionUrl is required for RemoteSessionPool')\n }\n\n const persistence = new RemoteSessionPersistence<AgentMessage>({\n baseUrl: sessionUrl,\n fetch: options.fetch,\n getAuth: options.getAuth,\n timeoutMs: options.timeoutMs ?? 30_000,\n userId: options.userId,\n wsKey: options.wsKey,\n })\n\n return new SessionPool(options, persistence, true)\n}\n\nexport function createInMemorySessionPool(options: SessionPoolOptions): SessionPool {\n return new SessionPool(options, new InMemorySessionPersistence())\n}\n\n/**\n * 用调用方注入的持久化后端建池(关系化 SQLite 等无法由 SessionStorageConfig 联合内建表达的后端)。\n * 让 `createAgentRuntime` 的 `kind:'custom'` 分支托管这些后端,宿主不必再手搓平行 AgentRuntime。\n */\nexport function createCustomSessionPool(\n options: SessionPoolOptions,\n persistence: SessionPersistence<AgentMessage>,\n autoPersist = true,\n): SessionPool {\n return new SessionPool(options, persistence, autoPersist)\n}\n","import { createDefaultProviderRegistry, type ProviderRegistry } from '@x-otto/ai'\nimport { createHookRegistry, provisionBaselineSecurity, type HookRegistry } from '@x-otto/hooks'\nimport { createLogger, type Logger } from '@x-otto/shared'\nimport { buildEngineStores } from './engine-stores'\n\nimport { ContextSourceRegistry } from './context-source'\nimport { createEnvironmentContextSource } from './context-sources/environment'\nimport { createMemoryIndexContextSource } from './context-sources/memory-index'\n\nimport {\n createDiskSessionPool,\n createRemoteSessionPool,\n createInMemorySessionPool,\n createCustomSessionPool,\n type SessionPool,\n type SessionPoolOptions,\n type SessionPorts,\n} from './session/session-manager'\n\nimport {\n DiskPanelStatePersistence,\n DiskPasteStatePersistence,\n createInMemoryPanelStatePersistence,\n createInMemoryPasteStatePersistence,\n} from '@x-otto/persistence'\nimport { PanelStateStore } from './session/panel-state-store'\nimport type { ResidencyGovernor } from './session/residency-governor'\n\nimport type { AgentMessage, ClockPort, StreamFunction, TraceEvent } from '@x-otto/agent'\nimport type { SessionPersistence } from '@x-otto/session'\nimport type { AppendLog, PanelStatePersistence, PasteStatePersistence } from '@x-otto/persistence'\nimport type { AgentSession } from './session/agent-session'\nimport type { MemorySubsystem } from './session/memory-transform'\nimport type { CheckpointData } from './session/checkpoint'\nimport type { AgentSessionInfo, ResolvedSessionConfig } from './session/types'\nimport type { LeaseInspection } from '@x-otto/session'\nimport type { Devtools } from '@x-otto/devtools'\n\nexport type SessionStorageConfig =\n | { kind: 'in-memory' }\n | { kind: 'disk'; sessionDir: string }\n | {\n kind: 'remote'\n sessionUrl: string\n fetch: typeof globalThis.fetch\n getAuth: () => Promise<{ token: string }>\n timeoutMs?: number\n /** user identity id,非空时请求带 `X-Otto-User-Id` header 实现 session 分区。 */\n userId?: string\n /** workspace 分区键(缺省 'default')——修复(N3):remote 后端此前恒 'default',\n * 与本地 SQLite 后端按 workspace_key 分区行为不一致。见 RemoteSessionPoolOptions.wsKey。\n * 支持惰性 getter(App 构造期 workspaceRef 尚未解析完成)。 */\n wsKey?: string | (() => string | undefined)\n }\n | {\n /**\n * 调用方注入的持久化后端(关系化 SQLite 等联合内建无法表达的后端)。让 createAgentRuntime\n * 成为唯一装配权威——宿主不再手搓平行 AgentRuntime 字面量(消除装配漂移)。\n * trace/checkpoint store 由调用方经 options 注入(ownsXxx=false,dispose 不替其 close)。\n */\n kind: 'custom'\n persistence: SessionPersistence<AgentMessage>\n /** 缺省 true(与关系化后端既有行为一致)。 */\n autoPersist?: boolean\n /** 后端专属资源释放(如 SqliteSessionRepository.close());dispose 时在 sessions.dispose 后调用。 */\n onDispose?: () => void | Promise<void>\n /**\n * RFC-146:本地高效冷会话枚举策略透传(见 SessionPoolOptions.listColdSessionsOverride)。\n * 关系化 SQLite 分支(session-pool-factory.ts)经此注入,走 SqliteSessionRepository 单次\n * SQL 查询而非通用 listPaginated+load 慢路径。\n */\n listColdSessionsOverride?: (workspaceKey: string) => Promise<AgentSessionInfo[]>\n /**\n * 只读探测策略透传(见 SessionPoolOptions.isSessionWriteLocked)。关系化 SQLite 分支\n * 经此注入 SessionWriteLeaseManager.peekLockedByOther 的薄封装。\n */\n isSessionWriteLocked?: (sessionId: string) => boolean\n /**\n * RFC-171 D4:写租约诊断/强制释放策略透传(见 SessionPoolOptions 对应字段)。\n * 关系化 SQLite 分支经此注入 SessionWriteLeaseManager.inspect/forceRelease 的薄封装。\n */\n inspectSessionWriteLease?: (sessionId: string) => LeaseInspection\n forceReleaseSessionWriteLease?: (sessionId: string) => void\n /**\n * RFC-108 D3 面板态(todoList/editedFiles/subagents/drafts)独立持久化的落盘目录。\n * 2026-07-21 修复:`kind:'custom'`(coding 默认的关系化 SQLite 后端)此前不构造\n * PanelStateStore——面板态仅存内存,进程重启即丢(drafts e2e 恒红暴露的真实生产\n * 缺口,非测试环境问题)。调用方传入目录(惯例 `<sessionDir>/panel-state`)即启用,\n * 缺省不构造(保持 in-memory/remote 的既有降级行为)。\n */\n panelStateDir?: string\n /**\n * RFC-345 §D8:显式注入的面板态/粘贴态持久化后端(影子模式用 InMemory 实现)。\n * 优先于 panelStateDir——两者同时缺省时不构造 PanelStateStore(既有降级)。\n * 由 §D1 白名单点 session-pool-factory 在影子态注入内存实现,磁盘零写。\n */\n panelStatePersistence?: PanelStatePersistence\n pasteStatePersistence?: PasteStatePersistence\n }\n\nexport interface RuntimeOptions {\n /** LLM provider 注册表。缺省=createDefaultProviderRegistry()。 */\n providerRegistry?: ProviderRegistry\n /** hook 注册表(tool.execute / system.prompt.transform 等的容器)。缺省=default preset。 */\n hookRegistry?: HookRegistry\n logger?: Logger\n /** 工作区目录(environment ContextSource + 会话缺省根)。缺省=process.cwd()。 */\n workspaceDir?: string\n /** 确定性时钟端口。缺省=真实系统时钟。 */\n clock?: ClockPort\n /** trace 落盘 store。缺省=内存 append-log(零磁盘、实时观测)。 */\n traceStore?: AppendLog<TraceEvent>\n /** checkpoint 落盘 store。缺省=内存 append-log。 */\n checkpointStore?: AppendLog<CheckpointData>\n /** 自动上下文管理端口。注入后引擎按 token 阈值在循环内压缩;未注入即不自动压缩。 */\n memory?: MemorySubsystem\n /** 会话池容量上限。缺省由 SessionPool 取 env SESSION_MAX。 */\n maxSessions?: number\n idleTimeoutMs?: number\n /** 会话持久化后端。缺省=纯内存(不落盘)。 */\n storage?: SessionStorageConfig\n /** devtools 实例(可选调试端口)。缺省=无调试。 */\n devtools?: Devtools\n /** 仅用于 restore / restoreAll 路径的配置工厂。同一 runtime 所有会话共享同一缺省配置模板。 */\n configProvider?: (agentName?: string, modelId?: string) => ResolvedSessionConfig\n /**\n * RFC-178 触发器 C:驻留收紧系数源(可选)。组装层(app-lifecycle)把 MemoryGovernor\n * 分级信号写入其 setLevel,PersistenceSync 的 cap 执行读取其收紧系数。未注入=恒全值。\n */\n residencyGovernor?: ResidencyGovernor\n /** RFC-323 M4:驻留预算运行时配置 getter(settings 注入透传)。 */\n getResidencyConfig?: () => import('@x-otto/session-contract').ResidencyBudgetConfig | undefined\n}\n\nexport interface AgentRuntime {\n /** 统一派生入口:子 runtime / 宿主 / swarm / task-runner 皆经此(§4-11)。 */\n createSession(\n config: ResolvedSessionConfig & { id?: string },\n ports?: SessionPorts,\n ): Promise<AgentSession>\n /** 会话池(fork/replay/checkpoint/持久化同步等编排门面)。 */\n readonly sessions: SessionPool\n readonly hookRegistry: HookRegistry\n /** 引擎 stream(从 providerRegistry 派生,§4-11:子 runtime 用之而非各自 createStreamFunction)。 */\n readonly stream: StreamFunction\n /** ContextSource 有序注册表(D33):通用源已内置注册,coding 策略源由宿主追加。 */\n readonly contextSources: ContextSourceRegistry\n readonly traceStore: AppendLog<TraceEvent>\n readonly checkpointStore: AppendLog<CheckpointData>\n readonly clock?: ClockPort\n dispose(): Promise<void>\n}\n\n/**\n * 通用 ContextSource 注册表(D33 单一来源):environment + memory-index(有注入面时)。\n * createAgentRuntime 内置调用之;宿主的离线装配路径(不经 createAgentRuntime)亦复用,\n * 确保「App 经 rt.contextSources 构建注入 hooks」对所有后端一致——消灭 App 端的二次造表。\n */\nexport function createDefaultContextSourceRegistry(\n workspaceDir: string,\n memory?: MemorySubsystem,\n): ContextSourceRegistry {\n const ctxReg = new ContextSourceRegistry()\n ctxReg.register(createEnvironmentContextSource(workspaceDir))\n // memory 子系统(单一实现 MemoryManager)恒含上下文注入面——有 memory 即注册\n // memory-index 源,无需 hasMemoryInjection 鸭子探测。\n if (memory) {\n ctxReg.register(createMemoryIndexContextSource(workspaceDir, memory))\n }\n return ctxReg\n}\n\nexport function createAgentRuntime(options: RuntimeOptions = {}): AgentRuntime {\n const logger =\n options.logger ?? createLogger('otto-runtime', { level: 'info', destination: 'stdout' })\n const providerRegistry = options.providerRegistry ?? createDefaultProviderRegistry()\n // RFC-077 M3-B(D6):bare-runtime(未注入 hookRegistry = 无宿主 PermissionController)下沉\n // 权限策略引擎 + 基线安全策略为默认提供,safe-by-default。宿主注入自己的 hookRegistry 时由其\n // PermissionController 拥有 provision(provisionBaselineSecurity 幂等亦防双注册)。\n const hookRegistry = options.hookRegistry ?? createHookRegistry({ preset: 'default' })\n if (!options.hookRegistry) {\n provisionBaselineSecurity(hookRegistry)\n }\n const storage: SessionStorageConfig = options.storage ?? { kind: 'in-memory' }\n\n // trace/checkpoint 后端选择走单源 buildEngineStores(与 coding App 共用);remote 统一为 Http。\n // 资源所有权:runtime 仅在**未注入**时自建 trace/checkpoint store,\n // dispose() 只关自己造的——注入的由 caller 拥有其生命周期,runtime 不得替其 close。\n const ownsTraceStore = options.traceStore === undefined\n const ownsCheckpointStore = options.checkpointStore === undefined\n\n const { traceStore, checkpointStore } = buildEngineStores(\n storage.kind === 'disk'\n ? { sessionDir: storage.sessionDir }\n : storage.kind === 'remote'\n ? {\n remoteLog: {\n baseUrl: storage.sessionUrl,\n getAuth: storage.getAuth,\n fetch: storage.fetch,\n timeoutMs: storage.timeoutMs,\n },\n }\n : {},\n { traceStore: options.traceStore, checkpointStore: options.checkpointStore },\n )\n const workspaceDir = options.workspaceDir ?? process.cwd()\n\n const ctxReg = createDefaultContextSourceRegistry(workspaceDir, options.memory)\n\n // RFC-108 D3:面板态独立持久化——仅 disk 后端构造(与 session 落盘同目录,\n // 子目录 panel-state/ 隔离,避免和 sessions/trace/checkpoint 文件混杂)。\n // in-memory/remote/custom 后端暂不构造(未注入时 SessionPool 优雅降级,面板态\n // 仅存内存不落盘,行为与迁移前一致,不阻塞这些后端的现有行为)。\n //\n // RFC-338:paste 缓存用**独立后端 + 独立子目录** `panel-state/paste/`——\n // ① 不与面板态共用一份 JSON(面板态每次文件编辑就全量 load→save,塞进最大 8MB\n // 的粘贴负载会拖慢那条热路径);\n // ② 独立子目录而非 `<id>.paste.json` 同级文件——`DiskPersistence.list()` 以\n // `endsWith('.json')` 过滤,同级并存会让面板态那侧连带匹配到 paste 文件、\n // 并把 id 反解成 `<sessionId>.paste`(实测)。\n const panelStateStore = ((): PanelStateStore | undefined => {\n // RFC-345 §D8:影子态优先用注入的内存持久化(磁盘零写)。\n if (\n storage.kind === 'custom' &&\n (storage.panelStatePersistence || storage.pasteStatePersistence)\n ) {\n return new PanelStateStore(\n storage.panelStatePersistence ?? createInMemoryPanelStatePersistence(),\n storage.pasteStatePersistence ?? createInMemoryPasteStatePersistence(),\n )\n }\n const baseDir =\n storage.kind === 'disk'\n ? `${storage.sessionDir}/panel-state`\n : storage.kind === 'custom' && storage.panelStateDir\n ? storage.panelStateDir\n : undefined\n if (baseDir === undefined) return undefined\n return new PanelStateStore(\n new DiskPanelStatePersistence({ baseDir }),\n new DiskPasteStatePersistence({ baseDir: `${baseDir}/paste` }),\n )\n })()\n\n const managerOptions: SessionPoolOptions = {\n hookRegistry,\n providerRegistry,\n logger,\n memory: options.memory,\n traceStore,\n checkpointStore,\n clock: options.clock,\n maxSessions: options.maxSessions,\n idleTimeoutMs: options.idleTimeoutMs,\n devtools: options.devtools,\n configProvider: options.configProvider,\n panelStateStore,\n residencyGovernor: options.residencyGovernor,\n getResidencyConfig: options.getResidencyConfig,\n }\n\n const sessions: SessionPool =\n storage.kind === 'disk'\n ? createDiskSessionPool({ ...managerOptions, sessionDir: storage.sessionDir })\n : storage.kind === 'remote'\n ? createRemoteSessionPool({\n ...managerOptions,\n sessionUrl: storage.sessionUrl,\n fetch: storage.fetch,\n getAuth: storage.getAuth,\n timeoutMs: storage.timeoutMs,\n userId: storage.userId,\n wsKey: storage.wsKey,\n })\n : storage.kind === 'custom'\n ? createCustomSessionPool(\n {\n ...managerOptions,\n listColdSessionsOverride: storage.listColdSessionsOverride,\n isSessionWriteLocked: storage.isSessionWriteLocked,\n inspectSessionWriteLease: storage.inspectSessionWriteLease,\n forceReleaseSessionWriteLease: storage.forceReleaseSessionWriteLease,\n },\n storage.persistence,\n storage.autoPersist ?? true,\n )\n : createInMemorySessionPool(managerOptions)\n\n return {\n createSession: (config, ports) => sessions.createSession(config, ports),\n sessions,\n hookRegistry,\n stream: sessions.stream,\n contextSources: ctxReg,\n traceStore,\n checkpointStore,\n clock: options.clock,\n dispose: async () => {\n sessions.dispose()\n // 释放 runtime 自建的 store 句柄(File/Http 的 close 停 tail 轮询/连接;Memory 无 close 即 no-op)。\n // close 幂等,故即便 App 另行 close 同实例(app.ts stop)也安全。\n if (ownsTraceStore) await traceStore.close?.()\n if (ownsCheckpointStore) await checkpointStore.close?.()\n // custom 后端专属资源(如 SqliteSessionRepository.close())。\n if (storage.kind === 'custom') await storage.onDispose?.()\n },\n }\n}\n","import { getHeapStatistics } from 'node:v8'\n\nimport type { Logger } from '@x-otto/shared'\n\n/**\n * memory-governor.ts — RFC-115 M1:长驻进程内存自我感知。\n *\n * 背景:otto 支持 `--continue` 长驻会话,进程可连续运行数十小时。此前对自身堆内存\n * 增长零监控——唯一\"处理机制\"是 V8 自己在 old-space 撞上限时 `FatalProcessOutOfMemory`\n * 直接 SIGABRT,靠 RFC-085 重启陪跑被动兜底(且该窗口内 `app.stop()`/`saveAll` 完全没有\n * 机会执行,存在状态丢失风险)。见 RFC-115 §1。\n *\n * M1 范围:只做周期采样 + 分级 + 回调触发,不实现任何降级/重启动作本身(回调是否执行\n * 压缩/弹窗由调用方注入,M2/M3 接线)——这是刻意的里程碑切分(RFC-115-todo.md M1)。\n *\n * 不引入通用 `HealthMetricSampler` 接口(RFC-115 D7,经独立评审 P2-1 撤回):本类是\n * 内存专用的具体实现,YAGNI——渲染帧率等其他维度若未来需要,从两个具体实现里再提炼\n * 共性,不预先猜测未来的抽象形状。\n */\n\nexport type MemoryPressureLevel = 'healthy' | 'warning' | 'critical'\n\nexport interface MemoryGovernorOptions {\n /** 告警阈值(占 V8 heap_size_limit 的百分比)。默认 70,经 OTTO_MEMORY_WARN_PCT 覆盖。 */\n warnThresholdPct?: number\n /** 危险阈值(占 V8 heap_size_limit 的百分比)。默认 85,经 OTTO_MEMORY_HARD_PCT 覆盖。 */\n hardThresholdPct?: number\n /** 采样间隔(ms)。默认 30_000,经 OTTO_MEMORY_SAMPLE_INTERVAL_MS 覆盖。 */\n sampleIntervalMs?: number\n /** warning 分级下的采样间隔(ms)。默认 10_000,经 OTTO_MEMORY_WARNING_INTERVAL_MS 覆盖。RFC-133 D7。 */\n warningIntervalMs?: number\n /** critical 分级下的采样间隔(ms)。默认 5_000,经 OTTO_MEMORY_CRITICAL_INTERVAL_MS 覆盖。RFC-133 D7。 */\n criticalIntervalMs?: number\n /**\n * 软降级触发冷却期(ms,RFC-115 D4)。默认 300_000(5 分钟)。防止 usedPct 在阈值附近\n * 抖动(如 69%↔71%)导致 onWarning 被高频重复触发——压缩本身有 CPU/延迟代价,且刚压缩完\n * 内存短期回升是正常现象。冷却期内即使分级变化仍触发(healthy↔warning 往返),\n * 只是不调用 onWarning;level 状态本身不受冷却期影响,仍如实反映当前采样结果。\n * 只作用于 warning 级别;critical 级别(Layer 2 硬阈值)不受冷却期约束,应立即响应。\n */\n warnCooldownMs?: number\n /** 分级变化时触发(level 从非 warning/critical 变为该级别时各触发一次,非每次采样都触发)。 */\n onWarning?: () => void\n /**\n * critical 分级触发,且当前不处于重启后宽限期(见 `postRestartGraceMs`)时调用。\n * 传入触发时刻的采样结果(`tick()` 内部已算好,避免调用方重复 `sample()` 造成值漂移\n * 与多余开销——经独立评审 P1-2 修正)。\n */\n onCritical?: (sample: MemorySample) => void\n /**\n * critical 分级触发,但当前处于重启后宽限期内时调用(RFC-115 D5 残留风险缓解)——\n * 调用方应降级为非阻塞提示(如 TUI 常驻通知区),而非再次弹出确认重启 Dialog,\n * 避免\"重启→同样内存状态→立刻又弹窗\"的连续打断。`onCritical`/`onCriticalDuringGrace`\n * 互斥,同一次触发只调用其一。同样传入触发时刻的采样结果。\n */\n onCriticalDuringGrace?: (sample: MemorySample) => void\n /**\n * RFC-116 M3:任意分级变化时触发(含恢复到 healthy 的降级方向)——`onWarning`/\n * `onCritical`/`onCriticalDuringGrace` 均只在\"变为 warning/critical\"时触发,缺少\n * \"从 warning/critical 恢复到 healthy\"这个方向的信号。footer 常驻指示器(异常时\n * 才显示、健康时零宽度)需要感知这个恢复事件才能自行清除,故新增本回调覆盖全部\n * 变化方向。不受冷却期/宽限期约束(纯粹的状态变化通知,不驱动任何降级动作)。\n */\n onLevelChange?: (level: MemoryPressureLevel) => void\n /**\n * RFC-115 追补:每次 tick 都触发(不像 onWarning/onCritical 只在分级变化时触发)——\n * 供调用方把完整采样时间序列落 trace(诊断专用,见 engine-nodes.ts 的\n * 'process.memory.sample' 节点)。MemoryGovernor 本身不知道 trace 系统的存在,\n * 只负责把采样结果透传出去,写入哪里由调用方(App 层)决定,保持职责分离。\n */\n onSample?: (sample: MemorySample) => void\n logger?: Logger\n /** 时间源注入(测试用;对齐 scheduler.ts 的 `deps.now` 模式)。默认 `Date.now`。 */\n now?: () => number\n /**\n * 重启后宽限期(ms,RFC-115 D5)。默认 600_000(10 分钟)。宽限期从 MemoryGovernor\n * **构造时刻**起算——调用方(App)应只在检测到本次启动是重启恢复(如 `--continue`)\n * 时传入非零值;全新会话不传(默认 0 = 无宽限期,立即响应 critical)。\n */\n postRestartGraceMs?: number\n /**\n * RFC-167 D1/D5:critical 分级\"持续时长\"超时阈值(ms)。默认 900_000(15 分钟),\n * 经 `OTTO_MEMORY_CRITICAL_TIMEOUT_MS` 覆盖。\n *\n * 背景:`onCritical` 只在状态**跃迁**(healthy/warning → critical)时触发一次;若\n * critical 分级持续存在(内存单调爬升不再回落,不产生第二次跃迁),用户错过那一次\n * 弹窗后不会再收到任何提醒,直至 V8 物理 OOM(2026-07-15 事故实测:跃迁触发 3 小时\n * 20 分钟后崩溃,期间零第二次通知)。本字段驱动 `onCriticalTimeout` 的触发阈值,\n * 与\"状态跃迁检测\"(`onCritical`)完全独立、互不影响。\n */\n criticalTimeoutMs?: number\n /**\n * RFC-167 D1:critical 分级持续超过 `criticalTimeoutMs` 时触发(与 `onCritical` 的\n * \"状态跃迁触发\"正交——即使从未再次跃迁,只要持续时长超过阈值就会触发一次)。\n *\n * 同一次 critical 区间内只触发一次(`criticalEnteredAt` 归零前不重复触发,避免\n * 每个 tick 都调用);恢复到非 critical 分级后 `criticalEnteredAt` 清空,下次重新\n * 进入 critical 会重新计时、可以再次触发一次超时。\n *\n * 调用方(interactive-memory-governor.ts)据此实现\"Dialog 从未被响应则自动执行\n * 用户已看到过的默认建议(重启);已响应过(无论确认还是取消)则改走持续通知 +\n * 二级兜底\"的两分支路由(RFC-167 D2/D3)。\n */\n onCriticalTimeout?: (sample: MemorySample) => void\n}\n\n/** 单次采样结果——供 M1 日志与后续里程碑(M2/M3)复用同一份读数,不重复采样。 */\nexport interface MemorySample {\n level: MemoryPressureLevel\n heapUsed: number\n heapSizeLimit: number\n usedPct: number\n /**\n * RFC-178 P2(观察维度,不参与分级):常驻集大小(进程全部内存含 V8 堆外)。\n * 分级仍只看 heapUsed/heap_size_limit(针对 V8 FatalProcessOutOfMemory 这一实证\n * 事故形态,指标已两次验证正确);rss/external 仅落 trace 供诊断——覆盖容器\n * OOM killer(按 RSS 杀)与图片 Buffer/ArrayBuffer(external,不计入 heapUsed)\n * 两类 V8 堆内指标的盲区。同一 `process.memoryUsage()` 调用返回值,零额外开销。\n */\n rss: number\n /** V8 堆外的 C++ 对象/Buffer 内存(见 rss 注释——观察维度,不参与分级)。 */\n external: number\n /**\n * 采样时刻(epoch ms,独立评审 P2-2)——用 MemoryGovernor 内部注入的 `now`(测试可\n * mock),而非消费方(如 onSample 回调)自行调用 `Date.now()`。避免测试用 fake timer\n * 时,MemoryGovernor 采样用的是 mock 时间,但 trace 事件时间戳却是真实系统时间的\n * 不一致。\n */\n ts: number\n}\n\nconst DEFAULT_WARN_THRESHOLD_PCT = 70\nconst DEFAULT_HARD_THRESHOLD_PCT = 85\nconst DEFAULT_SAMPLE_INTERVAL_MS = 30_000\nconst DEFAULT_WARN_COOLDOWN_MS = 300_000\nconst DEFAULT_POST_RESTART_GRACE_MS = 600_000\nconst DEFAULT_WARNING_INTERVAL_MS = 10_000\nconst DEFAULT_CRITICAL_INTERVAL_MS = 5_000\n/** RFC-167 D5:critical 持续时长超时阈值默认 15 分钟——比 D4 的 warning 冷却期(5 分钟)\n * 更长(这是\"最后兜底\"而非\"常规响应\",需要给用户充分响应窗口),比事故实测的\n * 3 小时 20 分钟无提醒窗口短得多(收敛最坏情况暴露时间)。 */\nconst DEFAULT_CRITICAL_TIMEOUT_MS = 900_000\n\nfunction resolvePctEnv(envVar: string, fallback: number): number {\n const raw = process.env[envVar]\n if (!raw) return fallback\n const parsed = Number(raw)\n return Number.isFinite(parsed) && parsed > 0 && parsed <= 100 ? parsed : fallback\n}\n\n/** RFC-133 D7:解析毫秒级环境变量(数字、>0,否则回退默认值)。 */\nfunction resolveMsEnv(envVar: string, fallback: number): number {\n const raw = process.env[envVar]\n if (!raw) return fallback\n const parsed = Number(raw)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback\n}\n\n/**\n * 进程内存自我感知组件——App 生命周期内单例,`start()`/`dispose()` 对齐既有子系统模式\n * (如 `SchedulerService` 的 unref ticker,见 `packages/schedule/src/scheduler.ts`,\n * RFC-161 M2 已迁出 @x-otto/coding)。\n */\nexport class MemoryGovernor {\n /** RFC-324 D2:非 readonly——支持 settings load 后晚绑定(与 onWarning 等回调的晚绑定模式一致)。\n * 构造期先走 env/默认值;settings load 后若 env 未设,由 app 层调 updateThresholds 覆盖。 */\n private warnThresholdPct: number\n private hardThresholdPct: number\n private readonly sampleIntervalMs: number\n private readonly warningIntervalMs: number\n private readonly criticalIntervalMs: number\n private readonly warnCooldownMs: number\n /** 非 readonly——支持构造后晚绑定(session 在 App 构造时尚不存在,对齐 `setScheduleService` 的晚绑定模式)。 */\n private onWarning: (() => void) | undefined\n private onCritical: ((sample: MemorySample) => void) | undefined\n private readonly logger: Logger | undefined\n private readonly now: () => number\n private readonly postRestartGraceMs: number\n\n private ticker: ReturnType<typeof setTimeout> | null = null\n private lastLevel: MemoryPressureLevel = 'healthy'\n /** 上一次 onWarning 实际触发的时间戳(epoch ms);undefined = 从未触发过。RFC-115 D4 冷却期基准。 */\n private lastWarningFiredAt: number | undefined\n /** 宽限期起点(epoch ms);undefined = 不处于宽限期(全新会话,或宽限期未由调用方设置)。 */\n private graceStartedAt: number | undefined\n private onCriticalDuringGrace: ((sample: MemorySample) => void) | undefined\n private onLevelChange: ((level: MemoryPressureLevel) => void) | undefined\n private onSample: ((sample: MemorySample) => void) | undefined\n /** RFC-133 D7:dispose 竞态防护——tick() 执行期间同步 dispose 后防止继续调度新定时器。 */\n private disposed = false\n /** RFC-167 D1:critical 持续时长超时阈值(ms)。 */\n private readonly criticalTimeoutMs: number\n private onCriticalTimeout: ((sample: MemorySample) => void) | undefined\n /** 首次进入 critical 分级的时间戳(epoch ms);恢复非 critical 时清空。RFC-167 D1。 */\n private criticalEnteredAt: number | undefined\n /** 本次 critical 区间内是否已触发过一次 onCriticalTimeout(同一区间只触发一次)。 */\n private criticalTimeoutFired = false\n\n constructor(options: MemoryGovernorOptions = {}) {\n this.warnThresholdPct = options.warnThresholdPct ?? resolvePctEnv('OTTO_MEMORY_WARN_PCT', DEFAULT_WARN_THRESHOLD_PCT)\n this.hardThresholdPct = options.hardThresholdPct ?? resolvePctEnv('OTTO_MEMORY_HARD_PCT', DEFAULT_HARD_THRESHOLD_PCT)\n this.sampleIntervalMs = options.sampleIntervalMs ?? resolveMsEnv('OTTO_MEMORY_SAMPLE_INTERVAL_MS', DEFAULT_SAMPLE_INTERVAL_MS)\n this.warningIntervalMs = options.warningIntervalMs ?? resolveMsEnv('OTTO_MEMORY_WARNING_INTERVAL_MS', DEFAULT_WARNING_INTERVAL_MS)\n this.criticalIntervalMs = options.criticalIntervalMs ?? resolveMsEnv('OTTO_MEMORY_CRITICAL_INTERVAL_MS', DEFAULT_CRITICAL_INTERVAL_MS)\n this.warnCooldownMs = options.warnCooldownMs ?? DEFAULT_WARN_COOLDOWN_MS\n this.postRestartGraceMs = options.postRestartGraceMs ?? DEFAULT_POST_RESTART_GRACE_MS\n this.criticalTimeoutMs = options.criticalTimeoutMs ?? resolveMsEnv('OTTO_MEMORY_CRITICAL_TIMEOUT_MS', DEFAULT_CRITICAL_TIMEOUT_MS)\n this.onWarning = options.onWarning\n this.onCritical = options.onCritical\n this.onCriticalDuringGrace = options.onCriticalDuringGrace\n this.onLevelChange = options.onLevelChange\n this.onSample = options.onSample\n this.onCriticalTimeout = options.onCriticalTimeout\n this.logger = options.logger\n this.now = options.now ?? (() => Date.now())\n }\n\n /** 当前分级(供外部只读查询,如 TUI 状态展示)。 */\n get currentLevel(): MemoryPressureLevel {\n return this.lastLevel\n }\n\n /**\n * 晚绑定 onWarning/onCritical/onCriticalDuringGrace(App 构造时活跃 session 尚不存在,\n * M2/M3 在 session 就绪后调用注入)。对齐 `App.setScheduleService` 的晚绑定模式,\n * 非构造期可选依赖。\n */\n /**\n * 合并式更新(每个字段独立覆盖,未传的字段保留原值)——RFC-115 追补:`app.ts`\n * 启动期注入 `onSample`(trace 落盘)后,`interactive.ts` 会话就绪时再调用一次\n * 注入 `onWarning`/`onCritical` 等;若采用整体覆盖语义,后一次调用会把前一次\n * 设置的 `onSample` 清空。多个调用方各自只关心自己负责的字段,改为逐字段合并,\n * 避免调用顺序耦合。\n */\n setCallbacks(callbacks: {\n onWarning?: () => void\n onCritical?: (sample: MemorySample) => void\n onCriticalDuringGrace?: (sample: MemorySample) => void\n onLevelChange?: (level: MemoryPressureLevel) => void\n onSample?: (sample: MemorySample) => void\n onCriticalTimeout?: (sample: MemorySample) => void\n }): void {\n if ('onWarning' in callbacks) this.onWarning = callbacks.onWarning\n if ('onCritical' in callbacks) this.onCritical = callbacks.onCritical\n if ('onCriticalDuringGrace' in callbacks) this.onCriticalDuringGrace = callbacks.onCriticalDuringGrace\n if ('onLevelChange' in callbacks) this.onLevelChange = callbacks.onLevelChange\n if ('onSample' in callbacks) this.onSample = callbacks.onSample\n if ('onCriticalTimeout' in callbacks) this.onCriticalTimeout = callbacks.onCriticalTimeout\n }\n\n /**\n * RFC-115 D5:标记本次启动为重启恢复(如 `--continue`),从当前时刻起算宽限期\n * `postRestartGraceMs`。宽限期内 critical 触发走 `onCriticalDuringGrace`(降级通知)\n * 而非 `onCritical`(弹窗)。调用方(interactive.ts)应仅在检测到 `--continue` 时调用;\n * 全新会话不调用,`onCritical` 立即生效无宽限期。\n */\n markRestarted(): void {\n this.graceStartedAt = this.now()\n }\n\n /** RFC-324 D2:settings load 后晚绑定阈值(优先级 env > config > 默认)。\n * 仅当对应 env 未设时才覆盖——env 设了则构造期已读 env 值,此处不覆盖。 */\n updateThresholds(opts: { warnThresholdPct?: number; hardThresholdPct?: number }): void {\n if (opts.warnThresholdPct != null && process.env['OTTO_MEMORY_WARN_PCT'] === undefined) {\n this.warnThresholdPct = opts.warnThresholdPct\n }\n if (opts.hardThresholdPct != null && process.env['OTTO_MEMORY_HARD_PCT'] === undefined) {\n this.hardThresholdPct = opts.hardThresholdPct\n }\n }\n\n /** 当前是否处于重启后宽限期内(供测试/可观测查询)。 */\n get inPostRestartGrace(): boolean {\n if (this.graceStartedAt === undefined) return false\n return this.now() - this.graceStartedAt < this.postRestartGraceMs\n }\n\n /** 单次采样 + 分级(不依赖 ticker,供测试与 onDemand 查询复用)。 */\n sample(): MemorySample {\n const { heapUsed, rss, external } = process.memoryUsage()\n const { heap_size_limit: heapSizeLimit } = getHeapStatistics()\n const usedPct = heapSizeLimit > 0 ? (heapUsed / heapSizeLimit) * 100 : 0\n const level = this.grade(usedPct)\n return { level, heapUsed, heapSizeLimit, usedPct, rss, external, ts: this.now() }\n }\n\n private grade(usedPct: number): MemoryPressureLevel {\n if (usedPct >= this.hardThresholdPct) return 'critical'\n if (usedPct >= this.warnThresholdPct) return 'warning'\n return 'healthy'\n }\n\n /** 启动周期采样(unref,不阻止进程退出)。幂等——重复调用是 no-op。 */\n start(): void {\n if (this.ticker) return\n // RFC-133 D7:改用 setTimeout 自调度链(首次调度用 sampleIntervalMs,进程刚启动通常是 healthy)。\n this.ticker = setTimeout(() => this.tick(), this.sampleIntervalMs)\n this.ticker.unref?.()\n }\n\n private tick(): void {\n const result = this.sample()\n // 结构性防御(独立评审 P1-3):disposed 检查前置到 onSample 之前——Node.js\n // 单线程语义下 tick() 执行期间 dispose() 实际不可能被调度,当前无真实风险,\n // 但把检查放在所有回调触发之前更显式,不依赖读者理解单线程时序假设。\n if (this.disposed) return\n // 每次 tick 都触发(区别于 onWarning/onCritical/onLevelChange 只在分级变化时\n // 触发)——落 trace 需要完整时间序列,不只是状态跳变点。\n this.onSample?.(result)\n if (result.level !== this.lastLevel) {\n this.logger?.info(\n { from: this.lastLevel, to: result.level, usedPct: result.usedPct.toFixed(1), heapUsed: result.heapUsed, heapSizeLimit: result.heapSizeLimit },\n '[memory-governor] pressure level changed',\n )\n // RFC-116 M3:无条件触发(不受冷却期/宽限期约束)——覆盖 onWarning/onCritical/\n // onCriticalDuringGrace 均未覆盖的\"恢复到 healthy\"方向,供 footer 常驻指示器\n // 感知异常解除并自行清除显示。\n this.onLevelChange?.(result.level)\n if (result.level === 'warning') {\n // RFC-115 D4:冷却期只约束 onWarning 的实际触发,不影响 lastLevel 的如实记录——\n // level 状态本身仍反映真实采样结果,冷却期内的\"跳过\"只发生在回调调用这一步。\n const nowMs = this.now()\n const withinCooldown =\n this.lastWarningFiredAt !== undefined && nowMs - this.lastWarningFiredAt < this.warnCooldownMs\n if (!withinCooldown) {\n this.lastWarningFiredAt = nowMs\n this.onWarning?.()\n } else {\n this.logger?.info(\n { cooldownRemainingMs: this.warnCooldownMs - (nowMs - this.lastWarningFiredAt!) },\n '[memory-governor] onWarning skipped (within cooldown)',\n )\n }\n }\n // critical 不受冷却期约束(RFC-115 D4:硬阈值应立即响应),但受宽限期约束(D5):\n // 宽限期内走降级通知路径,避免\"重启→同样内存状态→立刻又弹窗\"的连续打断。\n if (result.level === 'critical') {\n if (this.inPostRestartGrace) this.onCriticalDuringGrace?.(result)\n else this.onCritical?.(result)\n }\n }\n\n // RFC-167 D1:critical 持续时长跟踪——与上方\"状态跃迁\"检测完全独立(不受 if\n // 跃迁分支门控,每次 tick 都求值)。这是修复\"onCritical 只在跃迁时触发一次,\n // 若内存单调爬升不再产生第二次跃迁,用户再也不会被提醒\"这个响应缺口的核心机制。\n if (result.level === 'critical') {\n if (this.criticalEnteredAt === undefined) {\n // 首次进入本次 critical 区间——记录起点,重置本区间的超时触发标记。\n this.criticalEnteredAt = this.now()\n this.criticalTimeoutFired = false\n } else if (\n !this.criticalTimeoutFired &&\n this.now() - this.criticalEnteredAt >= this.criticalTimeoutMs\n ) {\n this.criticalTimeoutFired = true\n this.onCriticalTimeout?.(result)\n }\n } else {\n // 恢复到非 critical——清空计时器,下次重新进入 critical 会重新计时、可再次触发。\n this.criticalEnteredAt = undefined\n this.criticalTimeoutFired = false\n }\n\n this.lastLevel = result.level\n\n // RFC-133 D7:setTimeout 自调度链——根据当前分级决定下次采样间隔。\n // healthy 用 sampleIntervalMs,warning/critical 收紧到 warningIntervalMs/criticalIntervalMs。\n if (this.disposed) return\n const nextDelay = this.lastLevel === 'critical' ? this.criticalIntervalMs\n : this.lastLevel === 'warning' ? this.warningIntervalMs\n : this.sampleIntervalMs\n this.ticker = setTimeout(() => this.tick(), nextDelay)\n this.ticker.unref?.()\n }\n\n /** 停止采样,清 timeout。App.stop() 中调用。幂等。 */\n dispose(): void {\n // RFC-133 D7:先设 disposed 标志位再 clearTimeout——防止 tick() 执行期间同步\n // dispose(如 onCritical 回调内部触发重启流程→app.stop()→dispose())后,tick()\n // 尾部继续调度新 setTimeout 造成定时器泄漏。disposed 标志在调度前检查,确保\n // dispose 后不再产生任何新定时器。\n this.disposed = true\n if (this.ticker) {\n clearTimeout(this.ticker)\n this.ticker = null\n }\n }\n}\n","/**\n * fleet-monitor.ts — RFC-196 M2(D3):机器级 otto 进程群预算感知。\n *\n * 与 MemoryGovernor(单进程 V8 堆采样,RFC-115)刻意分离的独立组件——审核 F4:\n * MemoryGovernor 是堆专用采样器(RFC-115 D7 拒绝过通用 HealthMetricSampler 抽象),\n * 塞入 `ps` 枚举会破坏单一职责与可测性。本类结构对齐其回调模式(setCallbacks/\n * onSample/start/dispose + unref 定时器),但采样维度、周期(10min vs 30s)、\n * 依赖(进程枚举 vs process.memoryUsage)完全正交,互不感知。\n *\n * 语义边界:采样级统计只做**枚举近似**(进程数/总 RSS/疑似孤儿计数=ppid1+无tty),\n * 不做 fleet-scan 的租约级三态判定——那是 `otto ps` 的职责(用户显式动作,需要\n * 精确;采样是趋势观测,近似够用且零 @x-otto/session 读锁 IO)。\n *\n * 预算超限 → 一次性 warning 通知(去重:连续超限只通知一次,恢复后可再触发——\n * 对齐 MemoryGovernor onLevelChange 的方向对称语义)。不做任何自动动作(RFC-196\n * 非目标:不自动 kill)。`ps` 枚举失败 fail-soft 跳过本轮(不影响堆治理)。\n */\nimport { listOttoProcesses } from '@x-otto/env'\n\nimport type { Logger } from '@x-otto/shared'\nimport type { OttoProcessInfo } from '@x-otto/env'\n\nexport interface FleetSample {\n /** 全机 otto 主进程数。 */\n readonly processCount: number\n /** 总 RSS(MB,`ps` 口径含共享库——趋势观测用,勿作精算)。 */\n readonly totalRssMb: number\n /** 疑似孤儿计数(ppid==1 且无 TTY 的枚举近似,非租约级判定)。 */\n readonly suspectedOrphanCount: number\n /** 进程中最大代际计数(--restart-generation,M3 后有值;无则 0)。 */\n readonly maxGeneration: number\n /** 采样时刻(epoch ms,注入 now 时间源)。 */\n readonly ts: number\n}\n\nexport interface FleetMonitorOptions {\n /** 采样间隔(ms)。默认 600_000(10 分钟),经 OTTO_FLEET_SAMPLE_INTERVAL_MS 覆盖。 */\n sampleIntervalMs?: number\n /** 进程数预算。默认 8,经 OTTO_FLEET_MAX_PROCESSES 覆盖。 */\n maxProcesses?: number\n /** 总 RSS 预算(MB)。默认 8192,经 OTTO_FLEET_MAX_TOTAL_RSS_MB 覆盖。 */\n maxTotalRssMb?: number\n /** 每次采样触发(落 trace 用,完整时间序列)。 */\n onSample?: (sample: FleetSample) => void\n /** 预算首次超限触发(去重后);恢复到预算内后再次超限会再触发。 */\n onBudgetExceeded?: (sample: FleetSample, reason: 'process-count' | 'total-rss') => void\n /** 进程枚举(测试注入)。默认 listOttoProcesses。 */\n listProcesses?: () => OttoProcessInfo[]\n logger?: Logger\n /** 时间源注入(测试用,对齐 MemoryGovernor 的 deps.now 模式)。 */\n now?: () => number\n}\n\nconst DEFAULT_SAMPLE_INTERVAL_MS = 600_000\nconst DEFAULT_MAX_PROCESSES = 8\nconst DEFAULT_MAX_TOTAL_RSS_MB = 8192\n\nfunction resolveNumEnv(envVar: string, fallback: number): number {\n const raw = process.env[envVar]\n if (!raw) return fallback\n const parsed = Number(raw)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback\n}\n\nexport class FleetMonitor {\n private readonly sampleIntervalMs: number\n private readonly maxProcesses: number\n private readonly maxTotalRssMb: number\n private readonly listProcesses: () => OttoProcessInfo[]\n private readonly logger: Logger | undefined\n private readonly now: () => number\n\n private onSample: ((sample: FleetSample) => void) | undefined\n private onBudgetExceeded: ((sample: FleetSample, reason: 'process-count' | 'total-rss') => void) | undefined\n\n private ticker: ReturnType<typeof setTimeout> | null = null\n private disposed = false\n /** 超限通知去重:当前是否处于\"已通知过的超限区间\"内。恢复预算内后清零可再触发。 */\n private budgetExceededNotified = false\n\n constructor(options: FleetMonitorOptions = {}) {\n this.sampleIntervalMs = options.sampleIntervalMs ?? resolveNumEnv('OTTO_FLEET_SAMPLE_INTERVAL_MS', DEFAULT_SAMPLE_INTERVAL_MS)\n this.maxProcesses = options.maxProcesses ?? resolveNumEnv('OTTO_FLEET_MAX_PROCESSES', DEFAULT_MAX_PROCESSES)\n this.maxTotalRssMb = options.maxTotalRssMb ?? resolveNumEnv('OTTO_FLEET_MAX_TOTAL_RSS_MB', DEFAULT_MAX_TOTAL_RSS_MB)\n this.listProcesses = options.listProcesses ?? listOttoProcesses\n this.onSample = options.onSample\n this.onBudgetExceeded = options.onBudgetExceeded\n this.logger = options.logger\n this.now = options.now ?? (() => Date.now())\n }\n\n /** 合并式回调更新(逐字段,对齐 MemoryGovernor.setCallbacks 语义)。 */\n setCallbacks(callbacks: {\n onSample?: (sample: FleetSample) => void\n onBudgetExceeded?: (sample: FleetSample, reason: 'process-count' | 'total-rss') => void\n }): void {\n if ('onSample' in callbacks) this.onSample = callbacks.onSample\n if ('onBudgetExceeded' in callbacks) this.onBudgetExceeded = callbacks.onBudgetExceeded\n }\n\n /** 单次采样(不依赖 ticker,供测试/onDemand 查询)。枚举失败返回 undefined(fail-soft)。 */\n sample(): FleetSample | undefined {\n let processes: OttoProcessInfo[]\n try {\n processes = this.listProcesses()\n } catch (err) {\n this.logger?.warn(\n { err: err instanceof Error ? err.message : String(err) },\n '[fleet-monitor] process enumeration failed (skipping sample)',\n )\n return undefined\n }\n if (processes.length === 0) return undefined // 枚举异常(至少该有自己)——同样跳过\n\n let totalRssKb = 0\n let suspectedOrphanCount = 0\n let maxGeneration = 0\n for (const p of processes) {\n totalRssKb += p.rssKb\n if (p.ppid === 1 && p.tty === '??') suspectedOrphanCount++\n if (p.generation !== undefined && p.generation > maxGeneration) maxGeneration = p.generation\n }\n return {\n processCount: processes.length,\n totalRssMb: Math.round(totalRssKb / 1024),\n suspectedOrphanCount,\n maxGeneration,\n ts: this.now(),\n }\n }\n\n private tick(): void {\n if (this.disposed) return\n const sample = this.sample()\n if (sample) {\n this.onSample?.(sample)\n\n const overProcesses = sample.processCount > this.maxProcesses\n const overRss = sample.totalRssMb > this.maxTotalRssMb\n if (overProcesses || overRss) {\n if (!this.budgetExceededNotified) {\n this.budgetExceededNotified = true\n const reason = overProcesses ? 'process-count' : 'total-rss'\n this.logger?.warn(\n { processCount: sample.processCount, totalRssMb: sample.totalRssMb, maxProcesses: this.maxProcesses, maxTotalRssMb: this.maxTotalRssMb },\n '[fleet-monitor] fleet budget exceeded',\n )\n this.onBudgetExceeded?.(sample, reason)\n }\n } else {\n // 恢复预算内——清零去重标记,下次超限可再通知(方向对称)。\n this.budgetExceededNotified = false\n }\n }\n\n if (this.disposed) return\n this.ticker = setTimeout(() => this.tick(), this.sampleIntervalMs)\n this.ticker.unref?.()\n }\n\n /** 启动周期采样(unref 不阻止进程退出)。幂等。首轮延迟一个完整间隔(启动期不抢 CPU)。 */\n start(): void {\n if (this.ticker || this.disposed) return\n this.ticker = setTimeout(() => this.tick(), this.sampleIntervalMs)\n this.ticker.unref?.()\n }\n\n /** 停止采样。幂等。 */\n dispose(): void {\n this.disposed = true\n if (this.ticker) {\n clearTimeout(this.ticker)\n this.ticker = null\n }\n }\n}\n","import { spawnSync } from 'node:child_process'\n\n/**\n * 进程组 kill 原语 —— 单一真源。\n *\n * post-RFC-092 review F3:ProcessRegistry(bash `run_in_background` 的产品语义——\n * bg_xxx 句柄/端口嗅探/session 维度列表/UI 展示)与 ProcessRuntime(MCP/LSP/plugin/\n * grep 等内部基础设施进程的通用管理,category/lifecycle 模型)服务不同的消费面,\n * 语义分层是有意为之、不合并(此前尝试过完整合并、建过兼容 shim,因零消费已删除——\n * 详见 process-runtime.ts 底部注释)。但两者的 kill 基础设施逐字同源重复,抽出本模块\n * 消除重复,高层 API 各自保留。\n */\n\n/** 判断进程是否存活。 */\nexport const isAlive = (pid: number): boolean => {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n\n/** 向进程组发信号(负 pgid);组长已退则回退单 pid;都失败即已消失。 */\nexport const signalGroup = (pgid: number, signal: NodeJS.Signals): void => {\n try {\n process.kill(-pgid, signal)\n } catch {\n try {\n process.kill(pgid, signal)\n } catch {\n /* already gone */\n }\n }\n}\n\n/** Windows 无进程组:taskkill /T 力杀整棵树。 */\nexport const killTreeWindows = (pid: number): void => {\n try {\n spawnSync('taskkill', ['/F', '/T', '/PID', `${pid}`], { stdio: 'ignore', timeout: 5_000 })\n } catch {\n /* already gone */\n }\n}\n","import type { ChildProcess } from 'node:child_process'\nimport { isAlive, signalGroup, killTreeWindows } from './process-kill-primitives'\n\n/**\n * 后台服务进程注册表 + 确定性清理。\n *\n * 设计依据:\n * - 清理走\"注册表 + 宿主信号\",**不走** `session.deleted` hook —— 后者正常 CLI\n * 退出不 emit(SessionPool.dispose 只 dispose+清 map)。清理注册表无条件、\n * 跑在任何 hook 之前。\n * - 进程组杀用 killpg:spawn 时 detached → 子进程为组长,kill(-pgid)\n * 级联整棵子树。\n * - **不按 idle 时间杀**:dev server 静默不等于该死;仅撞满上限时拒绝/LRU。\n *\n * 与 process-runtime.ts(ProcessRuntime)的关系(post-RFC-092 review F3):\n * 两者服务不同消费面——本模块是 bash `run_in_background` 的产品语义(bg_xxx 句柄/\n * 端口嗅探/session 维度列表/UI 展示),ProcessRuntime 是 MCP/LSP/plugin/grep 等内部\n * 基础设施进程的通用管理。有意分层不合并;kill 基础设施抽至 process-kill-primitives.ts\n * 单一真源,消除此前的同源重复。\n */\n\nconst KILL_GRACE_MS = 2_000\nconst DEFAULT_MAX_PROCESSES = 64\n\nexport type BackgroundProcessStatus = 'running' | 'killing' | 'exited' | 'killed'\n\n/**\n * RFC-223:委托后台进程的服务协议分类——从运行时输出识别,best-effort。\n * 结构上与 `@x-otto/tools` 的 `background-types.ts` 同名类型一致,但**不 import 它**——\n * 沿用本文件头注释既定的 tools↔runtime 解耦策略(两者都依赖 agent,互不依赖,靠结构化\n * 类型满足契约,避免循环依赖)。\n */\nexport type ServiceProtocol = 'http' | 'ws' | 'tcp'\n\n/** RFC-223:从后台进程运行时输出识别到的服务信息(url 优先,退化到纯端口)。 */\nexport interface DetectedService {\n url?: string\n port?: number\n protocol: ServiceProtocol\n}\n\nexport interface BackgroundProcess {\n /** 返回给模型的句柄,如 bg_xxx。 */\n id: string\n sessionId: string\n /** 归属宿主(App)标识:App.stop 据此只杀本 App 起的进程,不越界杀兄弟 App。register 时打标,\n * 与 session pool 成员无关 → 会话被驱逐后其进程仍归本 App。缺省 undefined(未分宿主)。 */\n owner?: string\n pid: number\n /** 进程组 id;unix detached 下 == pid,killpg 用。 */\n pgid: number\n command: string\n cwd: string\n /** 从参数/输出 best-effort 嗅探(可能缺)。 */\n port?: number\n url?: string\n /** 输出落临时文件路径(P1 由 bash 工具填)。 */\n logPath?: string\n startedAt: number\n lastUsed: number\n status: BackgroundProcessStatus\n exitCode?: number\n /** 显式保护,免被上限 LRU 驱逐。 */\n keepAlive?: boolean\n /** RFC-223:运行时输出识别到的服务信息(D2 去重规则见 `updateDetectedService`)。 */\n detectedService?: DetectedService\n}\n\nexport interface RegisterInput {\n id: string\n sessionId: string\n /** 归属宿主(App)标识,见 BackgroundProcess.owner。 */\n owner?: string\n /** 子进程句柄:用于监听自然退出 + 推导 pgid。 */\n child: ChildProcess\n command: string\n cwd: string\n logPath?: string\n port?: number\n url?: string\n keepAlive?: boolean\n /** 注入时钟,便于测试;缺省 Date.now。 */\n now?: number\n}\n\ninterface Entry {\n info: BackgroundProcess\n child: ChildProcess\n /** 已从表中移除/驱逐:其后到来的 child exit 不再对外通知(onExit 只对在册进程)。 */\n silenced?: boolean\n}\n\nexport interface ProcessRegistryOptions {\n maxProcesses?: number\n graceMs?: number\n /** 可注入的时钟,仅用于 lastUsed/startedAt 戳(测试用)。 */\n clock?: () => number\n}\n\nexport type ProcessListener = (process: BackgroundProcess) => void\n\nexport class ProcessRegistry {\n private readonly entries = new Map<string, Entry>()\n private readonly maxProcesses: number\n private readonly graceMs: number\n private readonly clock: () => number\n private readonly spawnListeners = new Set<ProcessListener>()\n private readonly exitListeners = new Set<ProcessListener>()\n\n constructor(options: ProcessRegistryOptions = {}) {\n this.maxProcesses = options.maxProcesses ?? DEFAULT_MAX_PROCESSES\n this.graceMs = options.graceMs ?? KILL_GRACE_MS\n this.clock = options.clock ?? (() => Date.now())\n }\n\n /** 订阅后台进程登记事件。返回退订函数。 */\n onSpawn(listener: ProcessListener): () => void {\n this.spawnListeners.add(listener)\n return () => this.spawnListeners.delete(listener)\n }\n\n /**\n * 订阅后台进程退出事件(自然退出或被杀都触发一次,单一来源)。返回退订函数。\n * 注意:宿主优雅关闭时应先退订再 killAll,避免 teardown 期误发通知。\n */\n onExit(listener: ProcessListener): () => void {\n this.exitListeners.add(listener)\n return () => this.exitListeners.delete(listener)\n }\n\n private notify(listeners: Set<ProcessListener>, info: BackgroundProcess): void {\n for (const listener of [...listeners]) {\n try {\n listener(info)\n } catch {}\n }\n }\n\n /**\n * 登记后台进程。撞满上限时先驱逐最久未用的非 keepAlive 进程;若无可驱逐则抛错\n * (提示模型先 kill_shell)。\n */\n register(input: RegisterInput): BackgroundProcess {\n if (this.entries.size >= this.maxProcesses) {\n this.evictLeastRecent()\n }\n if (this.entries.size >= this.maxProcesses) {\n throw new Error(\n `Background process limit (${this.maxProcesses}) reached; stop one with kill_shell first.`,\n )\n }\n\n const pid = input.child.pid\n if (pid === undefined) {\n throw new Error('Cannot register a child process without a pid')\n }\n\n const now = input.now ?? this.clock()\n const info: BackgroundProcess = {\n id: input.id,\n sessionId: input.sessionId,\n owner: input.owner,\n pid,\n pgid: pid,\n command: input.command,\n cwd: input.cwd,\n port: input.port,\n url: input.url,\n logPath: input.logPath,\n startedAt: now,\n lastUsed: now,\n status: 'running',\n keepAlive: input.keepAlive,\n }\n const entry: Entry = { info, child: input.child }\n this.entries.set(input.id, entry)\n\n input.child.once('exit', (code) => {\n if (info.status === 'running') {\n info.status = 'exited'\n info.exitCode = code ?? undefined\n } else if (info.status === 'killing') {\n info.status = 'killed'\n }\n if (!entry.silenced) {\n this.notify(this.exitListeners, info)\n }\n })\n\n this.notify(this.spawnListeners, info)\n return info\n }\n\n get(id: string): BackgroundProcess | undefined {\n return this.entries.get(id)?.info\n }\n\n getForSession(sessionId: string, id: string): BackgroundProcess | undefined {\n const entry = this.entries.get(id)\n return entry && entry.info.sessionId === sessionId ? entry.info : undefined\n }\n\n list(): BackgroundProcess[] {\n return [...this.entries.values()].map((e) => e.info)\n }\n\n listForSession(sessionId: string): BackgroundProcess[] {\n return this.list().filter((p) => p.sessionId === sessionId)\n }\n\n touch(id: string): void {\n const entry = this.entries.get(id)\n if (entry) {\n entry.info.lastUsed = this.clock()\n }\n }\n\n /**\n * RFC-223 D2:运行时输出识别到新的服务信息时调用。去重规则——\"最近一条更优先\":\n * - 新结果含 url 而当前无 url → 覆盖。\n * - 新旧都含 url 但内容不同(如 localhost → 局域网地址)→ 覆盖为最新一条\n * (dev server 常见先打印 Local 后打印 Network,后者对想跨设备访问的用户更有用)。\n * - 新结果仅 port(无 url)而当前已有 url → 不覆盖(避免更差信息覆盖更好信息)。\n * - 其余情况(当前为空,或新旧完全相同)→ 直接采用新结果。\n * 进程已不在册(已退出/被驱逐/从未注册过该 id)时静默忽略,不抛错——识别回调是\n * fire-and-forget 的旁路信号,注册表状态是唯一真源,找不到 entry 不代表错误。\n */\n updateDetectedService(id: string, service: DetectedService): void {\n const entry = this.entries.get(id)\n if (!entry) {\n return\n }\n const current = entry.info.detectedService\n if (current?.url && !service.url) {\n return\n }\n entry.info.detectedService = service\n }\n\n remove(id: string): void {\n const entry = this.entries.get(id)\n if (entry) {\n entry.silenced = true\n this.entries.delete(id)\n }\n }\n\n size(): number {\n return this.entries.size\n }\n\n /** 优雅杀单个:SIGTERM → grace → SIGKILL。status 机防重复杀(BP7)。 */\n async kill(id: string): Promise<void> {\n const entry = this.entries.get(id)\n if (!entry) {\n return\n }\n await this.killEntry(entry)\n }\n\n /** 优雅杀光(宿主 SIGINT/SIGTERM 等优雅退出路径用)。 */\n async killAll(): Promise<void> {\n await Promise.all([...this.entries.values()].map((e) => this.killEntry(e)))\n }\n\n /**\n * 优雅杀某宿主(App)起的全部进程(App.stop 用)。按 register 时打的 owner 标过滤——多 App 同进程时\n * 不越界杀兄弟 App,且涵盖会话已被 pool 驱逐但进程仍在的(owner 与 pool 成员无关)。\n */\n async killAllForOwner(owner: string): Promise<void> {\n await Promise.all(\n [...this.entries.values()]\n .filter((e) => e.info.owner === owner)\n .map((e) => this.killEntry(e)),\n )\n }\n\n /** 优雅杀某会话全部(SessionPool.dispose 用)。 */\n async killAllForSession(sessionId: string): Promise<void> {\n await Promise.all(\n [...this.entries.values()]\n .filter((e) => e.info.sessionId === sessionId)\n .map((e) => this.killEntry(e)),\n )\n }\n\n /**\n * 同步杀光(`process.on('exit')` 兜底用——退出回调**不能** await)。\n * 直接 SIGKILL 整组,best-effort。\n */\n killAllSync(): void {\n for (const entry of this.entries.values()) {\n this.killEntrySync(entry)\n }\n }\n\n /** 同步 SIGKILL 单个;已退/已杀则跳过。 */\n private killEntrySync(entry: Entry): void {\n const { info } = entry\n if (info.status === 'exited' || info.status === 'killed') {\n return\n }\n if (process.platform === 'win32') {\n killTreeWindows(info.pid)\n } else {\n signalGroup(info.pgid, 'SIGKILL')\n }\n info.status = 'killed'\n }\n\n /**\n * 撞满上限时驱逐:优先已退出的,其次最久未用的非 keepAlive 进程(同步杀 + 移除)。\n * 全为 keepAlive 时不驱逐(register 随后抛错)。\n */\n private evictLeastRecent(): void {\n let victim: Entry | undefined\n for (const entry of this.entries.values()) {\n if (entry.info.status === 'exited' || entry.info.status === 'killed') {\n victim = entry\n break\n }\n if (entry.info.keepAlive) {\n continue\n }\n if (!victim || entry.info.lastUsed < victim.info.lastUsed) {\n victim = entry\n }\n }\n if (!victim) {\n return\n }\n victim.silenced = true\n this.killEntrySync(victim)\n this.entries.delete(victim.info.id)\n }\n\n private async killEntry(entry: Entry): Promise<void> {\n const { info } = entry\n if (info.status !== 'running') {\n return\n }\n info.status = 'killing'\n\n if (process.platform === 'win32') {\n killTreeWindows(info.pid)\n info.status = 'killed'\n return\n }\n\n signalGroup(info.pgid, 'SIGTERM')\n const exited = await this.waitForExit(entry, this.graceMs)\n if (!exited) {\n signalGroup(info.pgid, 'SIGKILL')\n }\n info.status = 'killed'\n }\n\n private waitForExit(entry: Entry, ms: number): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n let settled = false\n let timer: ReturnType<typeof setTimeout> | undefined\n const done = (exited: boolean) => {\n if (settled) {\n return\n }\n settled = true\n clearTimeout(timer)\n entry.child.removeListener('exit', onExit)\n resolve(exited)\n }\n const onExit = () => done(true)\n entry.child.once('exit', onExit)\n if (!isAlive(entry.info.pid)) {\n done(true)\n return\n }\n timer = setTimeout(() => done(false), ms)\n })\n }\n}\n\n/** 进程级单例:宿主与工具层共享同一注册表。 */\nexport const globalProcessRegistry = new ProcessRegistry()\n\nlet reaperInstalled = false\n\n/**\n * 安装宿主退出兜底:`process.on('exit')` → 同步 killAllSync。\n * 幂等。**不**安装 SIGINT/SIGTERM(交互式 TUI 自有处理;非交互宿主在各自\n * shutdown 路径里调 killAll)。\n */\nexport function installProcessReaper(registry: ProcessRegistry = globalProcessRegistry): void {\n if (reaperInstalled) {\n return\n }\n reaperInstalled = true\n process.on('exit', () => {\n registry.killAllSync()\n })\n}\n","import { spawn as nodeSpawn, spawnSync } from 'node:child_process'\nimport type { ChildProcess, SpawnOptions } from 'node:child_process'\nimport { EventEmitter } from 'node:events'\nimport { buildAllowedEnv } from '@x-otto/shared'\n\nimport { isAlive, signalGroup, killTreeWindows } from './process-kill-primitives'\n\n// ═══ 进程组 kill 工具 ═══\n// post-RFC-092 review F3:此前与 process-registry.ts 各自实现一份同源 kill 逻辑,\n// 已抽至 process-kill-primitives.ts 单一真源。ProcessRuntime 与 ProcessRegistry 的\n// 高层 API 仍分层不合并(消费面不同,见 process-registry.ts 头部注释)。\n\n// ═══ 类型 ═══\n\nexport type ProcessCategory = 'mcp' | 'lsp' | 'shell' | 'plugin' | 'browser' | 'tool' | 'cli'\n\nexport type ProcessStatus = 'starting' | 'running' | 'killing' | 'killed' | 'exited' | 'errored'\n\nexport type ProcessLifecycle = 'pinned' | 'evictable'\n\nexport interface ProcessOwner {\n /** 归属类型:mcp-server / lsp-server / plugin-hook / plugin-script / agent-tool / cli-shell / cli-chaperone / browser / notification / background-bash */\n type: string\n /** 归属 id:mcp server name / plugin id / session id */\n id: string\n}\n\nexport interface ProcessSpawnConfig {\n command: string\n args?: string[]\n options?: SpawnOptions\n\n /** 必填:谁 spawn 的 */\n owner: ProcessOwner\n /** 关联 session */\n sessionId?: string\n /** 进程分类 */\n category: ProcessCategory\n\n /** 人类可读标签(调试/日志) */\n label?: string\n\n /** 生命周期:pinned(常驻,免 LRU 驱逐)/ evictable(短命,可驱逐)。默认 evictable */\n lifecycle?: ProcessLifecycle\n /** detached + 免批量 kill(OAuth 浏览器 / notify launcher)。与 lifecycle 正交 */\n keepAlive?: boolean\n\n /** 覆盖全局默认 SIGTERM→SIGKILL 宽限期(默认 2000ms) */\n killGraceMs?: number\n /** 硬上限(默认 30000ms) */\n killTimeoutMs?: number\n\n /** 外部取消信号 */\n signal?: AbortSignal\n /**\n * 环境变量覆盖。spawn() 会以此为 override 调 buildAllowedEnv(规则 7):\n * 白名单基座(PATH/HOME/LANG/… strip 凭证)+ 本 override,凭证永不 bypass。\n * 注意:registerChild 路径不经此(子进程已由调用方 spawn,env 已定)。\n */\n env?: Record<string, string>\n cwd?: string\n}\n\nexport interface KillOptions {\n graceMs?: number\n timeoutMs?: number\n reason?: string\n}\n\nexport interface ExitResult {\n code: number | null\n signal: NodeJS.Signals | null\n timedOut: boolean\n}\n\nexport interface ProcessFilter {\n owner?: ProcessOwner\n sessionId?: string\n category?: ProcessCategory\n}\n\n// ═══ ManagedProcess 接口(RFC §3.5) ═══\n\nexport interface ManagedProcess {\n readonly id: string\n readonly pid: number | undefined\n /** 进程组 id;unix detached 下 == pid,非-detached 下可能不等于 pid */\n readonly pgid: number | undefined\n readonly status: ProcessStatus\n readonly config: ProcessSpawnConfig\n\n /** 原始字节流 stdin/stdout/stderr(规则 5/H2:非行缓冲,帧解析由消费者负责) */\n readonly stdin: NodeJS.WritableStream | null\n readonly stdout: NodeJS.ReadableStream | null\n readonly stderr: NodeJS.ReadableStream | null\n\n /** 便利层;LSP/MCP 等帧协议应直接消费字节流。 */\n onStdoutLine(cb: (line: string) => void): () => void\n onStderrLine(cb: (line: string) => void): () => void\n onExit(cb: (result: ExitResult) => void): () => void\n onError(cb: (err: Error) => void): () => void\n\n /** 优雅杀:SIGTERM → grace → SIGKILL(Windows 走 taskkill;复用 signalGroup 单 pid 回退——C1) */\n kill(opts?: KillOptions): Promise<void>\n waitForExit(timeoutMs?: number): Promise<ExitResult>\n\n /** 上次活跃时间戳(LRU 驱逐用) */\n lastUsed: number\n}\n\n// ═══ 内部实现 ═══\n\ninterface ManagedProcessEntry {\n proc: ManagedProcessImpl\n child: ChildProcess\n}\n\nlet nextId = 0\n\nclass ManagedProcessImpl extends EventEmitter implements ManagedProcess {\n readonly id: string\n readonly child: ChildProcess\n config: ProcessSpawnConfig\n status: ProcessStatus = 'starting'\n private _pid: number | undefined\n private _pgid: number | undefined\n private _exitResult: ExitResult | null = null\n private _exitResolve: ((r: ExitResult) => void) | null = null\n private _stdoutBuf = ''\n private _stderrBuf = ''\n lastUsed: number\n\n constructor(child: ChildProcess, config: ProcessSpawnConfig) {\n super()\n this.id = `proc_${++nextId}_${Date.now()}`\n this.child = child\n this.config = config\n this._pid = child.pid\n this._pgid = child.pid // detached 下 == pid(组长),否则可能不准;kill 走 signalGroup 回退\n this.lastUsed = Date.now()\n\n // ── 进程事件绑定 ──\n\n this.child.on('spawn', () => {\n this._pid = this.child.pid\n this._pgid = this.child.pid\n if (this.status === 'starting') this.status = 'running'\n })\n\n this.child.on('error', (err: Error) => {\n this.lastUsed = Date.now()\n if (this.status === 'starting' || this.status === 'running') {\n this.status = 'errored'\n }\n this.emit('error', err)\n })\n\n this.child.on('exit', (code, signal) => {\n this.lastUsed = Date.now()\n const result: ExitResult = { code, signal, timedOut: false }\n this._exitResult = result\n if (this.status === 'running') {\n this.status = 'exited'\n } else if (this.status === 'killing') {\n this.status = 'killed'\n }\n this.emit('exit', result)\n if (this._exitResolve) {\n this._exitResolve(result)\n this._exitResolve = null\n }\n })\n\n // ── stdout/stderr 行缓冲(便利层)──\n // 不设 encoding——保持裸字节流(H2),onStdoutLine 便利层自行 toString\n\n if (this.child.stdout) {\n this.child.stdout.on('data', (chunk: Buffer) => {\n this.lastUsed = Date.now()\n const text = chunk.toString('utf-8')\n this._stdoutBuf += text\n for (;;) {\n const nl = this._stdoutBuf.indexOf('\\n')\n if (nl === -1) break\n const line = this._stdoutBuf.slice(0, nl).replace(/\\r$/, '')\n this._stdoutBuf = this._stdoutBuf.slice(nl + 1)\n this.emit('stdoutLine', line)\n }\n })\n }\n\n if (this.child.stderr) {\n this.child.stderr.on('data', (chunk: Buffer) => {\n this.lastUsed = Date.now()\n const text = chunk.toString('utf-8')\n this._stderrBuf += text\n for (;;) {\n const nl = this._stderrBuf.indexOf('\\n')\n if (nl === -1) break\n const line = this._stderrBuf.slice(0, nl).replace(/\\r$/, '')\n this._stderrBuf = this._stderrBuf.slice(nl + 1)\n this.emit('stderrLine', line)\n }\n })\n }\n\n // ── AbortSignal 支持 ──\n\n if (config.signal) {\n const onAbort = () => {\n void this.kill({ reason: 'aborted' })\n }\n if (config.signal.aborted) {\n onAbort()\n } else {\n config.signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n }\n\n get pid(): number | undefined {\n return this._pid\n }\n\n get pgid(): number | undefined {\n return this._pgid\n }\n\n get stdin(): NodeJS.WritableStream | null {\n return this.child.stdin && !this.child.stdin.destroyed ? this.child.stdin : null\n }\n\n get stdout(): NodeJS.ReadableStream | null {\n return this.child.stdout && !this.child.stdout.destroyed ? this.child.stdout : null\n }\n\n get stderr(): NodeJS.ReadableStream | null {\n return this.child.stderr && !this.child.stderr.destroyed ? this.child.stderr : null\n }\n\n onStdoutLine(cb: (line: string) => void): () => void {\n this.on('stdoutLine', cb)\n return () => this.off('stdoutLine', cb)\n }\n\n onStderrLine(cb: (line: string) => void): () => void {\n this.on('stderrLine', cb)\n return () => this.off('stderrLine', cb)\n }\n\n onExit(cb: (result: ExitResult) => void): () => void {\n this.on('exit', cb)\n return () => this.off('exit', cb)\n }\n\n onError(cb: (err: Error) => void): () => void {\n this.on('error', cb)\n return () => this.off('error', cb)\n }\n\n // ── kill(统一状态机 §3.3) ──\n\n async kill(opts?: KillOptions): Promise<void> {\n if (this.status === 'killed' || this.status === 'exited') return\n this.status = 'killing'\n this.lastUsed = Date.now()\n\n const grace = opts?.graceMs ?? this.config.killGraceMs ?? 2_000\n const pid = this._pid\n const pgid = this._pgid\n\n if (!pid || !isAlive(pid)) {\n this.status = 'killed'\n return\n }\n\n if (process.platform === 'win32') {\n killTreeWindows(pid)\n this.status = 'killed'\n return\n }\n\n // Unix:SIGTERM → grace → SIGKILL,走 signalGroup(C1:单 pid 回退)\n signalGroup(pgid ?? pid, 'SIGTERM')\n\n const exited = await new Promise<boolean>((resolve) => {\n let settled = false\n const done = (val: boolean) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n resolve(val)\n }\n\n const onChildExit = () => done(true)\n this.child.once('exit', onChildExit)\n\n if (!isAlive(pid)) {\n done(true)\n return\n }\n\n const timer = setTimeout(() => {\n this.child.removeListener('exit', onChildExit)\n done(false)\n }, grace)\n })\n\n if (!exited) {\n signalGroup(pgid ?? pid, 'SIGKILL')\n }\n this.status = 'killed'\n }\n\n async waitForExit(timeoutMs = 30_000): Promise<ExitResult> {\n if (this._exitResult) return this._exitResult\n\n return new Promise<ExitResult>((resolve) => {\n // 先检查是否已退出\n if (this._exitResult) {\n resolve(this._exitResult)\n return\n }\n\n let settled = false\n const done = (r: ExitResult) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n this.off('exit', onExit)\n resolve(r)\n }\n\n const onExit = (r: ExitResult) => done(r)\n this.on('exit', onExit)\n\n const timer = setTimeout(() => {\n done({ code: null, signal: null, timedOut: true })\n }, timeoutMs)\n })\n }\n}\n\n// ═══ ProcessRuntime ═══\n\nexport interface ProcessRuntimeOptions {\n maxProcesses?: number\n killGraceMs?: number\n killTimeoutMs?: number\n clock?: () => number\n}\n\nconst DEFAULT_MAX_PROCESSES = 128\nconst DEFAULT_KILL_GRACE_MS = 2_000\nconst DEFAULT_KILL_TIMEOUT_MS = 30_000\n\nexport class ProcessRuntime {\n private readonly _entries = new Map<string, ManagedProcessEntry>()\n private readonly _maxProcesses: number\n readonly killGraceMs: number\n readonly killTimeoutMs: number\n private readonly _clock: () => number\n private readonly _spawnListeners = new Set<(proc: ManagedProcess) => void>()\n private readonly _exitListeners = new Set<(proc: ManagedProcess) => void>()\n\n constructor(options: ProcessRuntimeOptions = {}) {\n this._maxProcesses = options.maxProcesses ?? DEFAULT_MAX_PROCESSES\n this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS\n this.killTimeoutMs = options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS\n this._clock = options.clock ?? (() => Date.now())\n }\n\n onSpawn(listener: (proc: ManagedProcess) => void): () => void {\n this._spawnListeners.add(listener)\n return () => this._spawnListeners.delete(listener)\n }\n\n onExit(listener: (proc: ManagedProcess) => void): () => void {\n this._exitListeners.add(listener)\n return () => this._exitListeners.delete(listener)\n }\n\n /** 池空时触发。 */\n onAllExited(listener: () => void): () => void {\n const check = () => {\n if (this._entries.size === 0) listener()\n }\n const wrapped = () => check()\n this._exitListeners.add(wrapped)\n // 初始检查\n check()\n return () => this._exitListeners.delete(wrapped)\n }\n\n // ── 核心 spawn ──\n\n /**\n * 启动子进程并返回 ManagedProcess 句柄。\n * 非-keepAlive 子进程强制 `detached: true`(C1/规则 4:使子进程成为组长,-pgid 生效)。\n */\n spawn(config: ProcessSpawnConfig): ManagedProcess {\n if (this._entries.size >= this._maxProcesses) {\n this._evictLeastRecent()\n }\n if (this._entries.size >= this._maxProcesses) {\n throw new Error(\n `Process limit (${this._maxProcesses}) reached. Stop unused processes first.`,\n )\n }\n\n const spawnOpts: SpawnOptions = {\n ...(config.options ?? {}),\n stdio: config.options?.stdio ?? ['pipe', 'pipe', 'pipe'],\n cwd: config.cwd ?? config.options?.cwd,\n // 规则 7:统一在此 apply buildAllowedEnv(白名单基座 + override),凭证不 bypass。\n // 各 spawn 消费者(MCP/plugin/…)无需各自记得白名单——集中强制,消除泄漏面。\n env: buildAllowedEnv(config.env ?? config.options?.env),\n // C1: 非-keepAlive 强制 detached → 子进程成为组长 → -pgid 生效\n ...(config.keepAlive !== true ? { detached: true } : {}),\n }\n\n const child = nodeSpawn(config.command, config.args ?? [], spawnOpts)\n const proc = new ManagedProcessImpl(child, config)\n\n this._entries.set(proc.id, { proc, child })\n\n // 进程退出时自动从池中移除(非 keepAlive 且非 pinned)\n proc.onExit(() => {\n const entry = this._entries.get(proc.id)\n if (!entry) return\n\n // cleanup:非 keepAlive 且非 pinned → 移除\n const lifecycle = config.lifecycle ?? 'evictable'\n if (config.keepAlive !== true && lifecycle !== 'pinned') {\n this._entries.delete(proc.id)\n }\n\n for (const listener of [...this._exitListeners]) {\n try {\n listener(proc)\n } catch {\n /* swallow */\n }\n }\n })\n\n // 通知 spawn 观察者\n for (const listener of [...this._spawnListeners]) {\n try {\n listener(proc)\n } catch {\n /* swallow */\n }\n }\n\n return proc\n }\n\n // ── 查询 ──\n\n get(id: string): ManagedProcess | undefined {\n return this._entries.get(id)?.proc\n }\n\n list(filter?: ProcessFilter): ManagedProcess[] {\n let entries = [...this._entries.values()]\n if (filter?.owner) {\n entries = entries.filter(\n (e) =>\n e.proc.config.owner.type === filter.owner!.type &&\n e.proc.config.owner.id === filter.owner!.id,\n )\n }\n if (filter?.sessionId) {\n entries = entries.filter((e) => e.proc.config.sessionId === filter.sessionId)\n }\n if (filter?.category) {\n entries = entries.filter((e) => e.proc.config.category === filter.category)\n }\n return entries.map((e) => e.proc)\n }\n\n listForSession(sessionId: string): ManagedProcess[] {\n return this.list({ sessionId })\n }\n\n listForOwner(owner: ProcessOwner): ManagedProcess[] {\n return this.list({ owner })\n }\n\n touch(id: string): void {\n const entry = this._entries.get(id)\n if (entry) entry.proc.lastUsed = this._clock()\n }\n\n remove(id: string): void {\n this._entries.delete(id)\n }\n\n size(): number {\n return this._entries.size\n }\n\n // ── 批量 kill ──\n\n /** kill 满足 filter 的非 keepAlive 进程。pinned 进程也杀——killAll 是显式操作非 LRU 驱逐。 */\n async killAll(filter?: ProcessFilter, opts?: KillOptions): Promise<void> {\n const targets = this.list(filter).filter((p) => !p.config.keepAlive)\n await Promise.all(targets.map((p) => p.kill(opts)))\n }\n\n /**\n * 同步 SIGKILL 所有非 keepAlive 进程。\n * `process.on('exit')` 兜底用——不声称覆盖信号退出(C2/规则 3)。\n * 复用 `signalGroup` 单 pid 回退(C1/规则 4),禁裸 `process.kill(-pid)`。\n * Windows 单次批量 taskkill(规则 9)。\n */\n killAllSync(): void {\n const isWindows = process.platform === 'win32'\n const pids: number[] = []\n\n for (const [id, entry] of this._entries) {\n const proc = entry.proc\n if (proc.config.keepAlive) continue\n if (proc.status !== 'running' && proc.status !== 'starting') continue\n\n const pid = proc.pid\n if (!pid) continue\n\n if (isWindows) {\n pids.push(pid)\n } else {\n // C1: 走 signalGroup 带单 pid 回退,禁裸 process.kill(-pid)\n signalGroup(proc.pgid ?? pid, 'SIGKILL')\n }\n this._entries.delete(id)\n }\n\n // Windows: 单次批量 taskkill(规则 9:不在 exit handler 逐进程 execSync 5s)\n if (isWindows && pids.length > 0) {\n try {\n spawnSync('taskkill', ['/F', '/T', ...pids.map(String).flatMap((p) => ['/PID', p])], {\n stdio: 'ignore',\n timeout: 5_000,\n })\n } catch {\n /* best-effort */\n }\n }\n }\n\n async killAllForOwner(ownerType: string, ownerId: string): Promise<void> {\n return this.killAll({ owner: { type: ownerType, id: ownerId } })\n }\n\n async killAllForSession(sessionId: string): Promise<void> {\n return this.killAll({ sessionId })\n }\n\n /**\n * 低级注册:包装一个已被外部 `child_process.spawn()` 创建的 ChildProcess 为 ManagedProcess。\n * 用于兼容 shim(旧 ProcessRegistry API 预 spawn 了 child)和后续子系统渐进迁移。\n *\n * 规则 4/C1:非-keepAlive 若未 detached,调用方应确保进程组长语义正确。\n */\n registerChild(config: ProcessSpawnConfig, child: ChildProcess): ManagedProcess {\n if (this._entries.size >= this._maxProcesses) {\n this._evictLeastRecent()\n }\n if (this._entries.size >= this._maxProcesses) {\n throw new Error(\n `Process limit (${this._maxProcesses}) reached. Stop unused processes first.`,\n )\n }\n\n const proc = new ManagedProcessImpl(child, config)\n this._entries.set(proc.id, { proc, child })\n\n proc.onExit(() => {\n const entry = this._entries.get(proc.id)\n if (!entry) return\n const lifecycle = config.lifecycle ?? 'evictable'\n if (config.keepAlive !== true && lifecycle !== 'pinned') {\n this._entries.delete(proc.id)\n }\n for (const listener of [...this._exitListeners]) {\n try { listener(proc) } catch { /* swallow */ }\n }\n })\n\n for (const listener of [...this._spawnListeners]) {\n try { listener(proc) } catch { /* swallow */ }\n }\n\n return proc\n }\n\n // ── 内部:LRU 驱逐(跳过 pinned + keepAlive) ──\n\n private _evictLeastRecent(): void {\n let victim: ManagedProcessEntry | undefined\n for (const entry of this._entries.values()) {\n const cfg = entry.proc.config\n const lifecycle = cfg.lifecycle ?? 'evictable'\n // 已退出/被杀的条目优先回收(任何 lifecycle)——死条目不占 live 配额。\n // 关键:pinned 只保护「存活」进程免 LRU 驱逐;一旦退出即可回收,\n // 否则 MCP/LSP 反复重连会让死 pinned 条目累积撑满池(reconnect 泄漏)。\n if (entry.proc.status === 'exited' || entry.proc.status === 'killed' || entry.proc.status === 'errored') {\n victim = entry\n break\n }\n // 存活的 keepAlive / pinned 免驱逐(H3/规则 6)\n if (cfg.keepAlive) continue\n if (lifecycle === 'pinned') continue\n if (!victim || entry.proc.lastUsed < victim.proc.lastUsed) {\n victim = entry\n }\n }\n if (!victim) return\n // 同步 SIGKILL + 移除\n const pid = victim.proc.pid\n if (pid && isAlive(pid)) {\n if (process.platform === 'win32') {\n killTreeWindows(pid)\n } else {\n signalGroup(victim.proc.pgid ?? pid, 'SIGKILL')\n }\n }\n this._entries.delete(victim.proc.id)\n }\n\n // 注:ProcessRegistry→ProcessRuntime 的兼容 shim(asLegacyRegistry / process-registry-shim.ts)\n // 曾为渐进迁移预留,但生产从未消费(旧消费者直用 globalProcessRegistry),零调用点,已删除。\n}\n\n// ═══ 全局单例 + exit reaper ═══\n\n/** 全局进程运行时单例。 */\nexport const globalProcessRuntime = new ProcessRuntime()\n\nlet _reaperInstalled = false\n\n/**\n * 安装宿主退出兜底:`process.on('exit')` → 同步 `killAllSync()`。\n * 幂等。不覆盖 SIGINT/SIGTERM——交互式 TUI 自有处理。\n *\n * **C2/规则 3**:不声称覆盖信号退出——`process.on('exit')` 在 SIGTERM/SIGKILL/SIGHUP 下不触发。\n * 正常 `process.exit()` 和可捕获崩溃路径才触发。\n */\nexport function installExitReaper(rt: ProcessRuntime = globalProcessRuntime): void {\n if (_reaperInstalled) return\n _reaperInstalled = true\n process.on('exit', () => {\n rt.killAllSync()\n })\n}\n","/**\n * git-worktree.ts —— RFC-023 Phase 1:agent-session 作业的 git worktree 隔离。\n *\n * 后台 agent 作业**绝不能**背着用户改主工作区(本会话 cli.ts 被并发改的事故即反例)。\n * 每个作业跑在独立 worktree(临时分支):agent 在 worktree 内自由改 → 产出 diff → 主会话\n * 评审后带冲突 apply(worktreePath/worktreeBranch 隔离 + apply-patch 回主区)。\n *\n * 纯 git 子进程封装(execFileSync 数组参数,无 shell 注入);不持状态。\n */\n\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, realpathSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface Worktree {\n /** worktree 绝对路径(agent 作业的 cwd / workspaceDir)。 */\n path: string\n /** 临时分支名。 */\n branch: string\n /** 建 worktree 时主仓 HEAD 的 sha(apply 时检漂移)。 */\n base: string\n /** 主仓根(remove 时用)。 */\n repoRoot: string\n}\n\nfunction git(cwd: string, args: string[]): string {\n return execFileSync('git', ['-C', cwd, ...args], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n }).trimEnd()\n}\n\n/**\n * 为作业建一个 worktree:基于主仓当前 HEAD 拉临时分支,落在系统临时目录(不污染主仓树)。\n *\n * L2 注意:worktree 落在 `os.tmpdir()` 下**有意为之**——沙箱可写根含 tmpdir(computeSandboxRoots),\n * 故沙箱开启时 agent 对 worktree 的写恰好被放行。若改 worktree 位置,须同步把该路径加入沙箱可写根,\n * 否则沙箱模式下 agent 作业写文件会被拒。\n */\nexport function createWorktree(repoRoot: string, jobId: string): Worktree {\n const base = git(repoRoot, ['rev-parse', 'HEAD'])\n const branch = `otto/job-${jobId}`\n const path = join(mkdtempSync(join(tmpdir(), 'otto-wt-')), `job-${jobId}`)\n git(repoRoot, ['worktree', 'add', '-b', branch, path, base])\n return { path, branch, base, repoRoot }\n}\n\n/**\n * 捕获 agent 在 worktree 内的全部改动为统一 diff(含未跟踪文件 + 删除)。\n * worktree 分支 HEAD == base(作业不 commit),故 stage 全部后 `diff --cached` 即作业产物。\n *\n * M2 已知限制:`git add -A` **遵守 .gitignore**——agent 创建的被忽略文件不进 diff、apply 不带过去\n * (这通常是想要的:忽略文件本就不该入库)。若将来需带忽略产物,改用 `add -A --force`。\n */\nexport function worktreeDiff(wt: Worktree): string {\n git(wt.path, ['add', '-A'])\n return execFileSync('git', ['-C', wt.path, 'diff', '--cached', '--binary'], {\n encoding: 'utf-8',\n // 默认 maxBuffer 1MB——大/二进制改动 diff 易超限 ENOBUFS 抛出整个作业失败;放宽到 256MB。\n maxBuffer: 256 * 1024 * 1024,\n })\n}\n\nexport interface ApplyDiffResult {\n applied: boolean\n /** 冲突/失败的文件路径(best-effort 从 git stderr 解析)。 */\n conflictPaths: string[]\n}\n\nfunction parseConflictPaths(stderr: string): string[] {\n return [\n ...new Set(\n stderr\n .split('\\n')\n .map(\n (l) =>\n /(?:patch failed: |error: )([^\\s:]+?)(?::| ?:?\\s*(?:patch does not apply|does not exist))/.exec(\n l,\n )?.[1],\n )\n .filter((p): p is string => !!p && p !== 'error'),\n ),\n ]\n}\n\n/**\n * review J6:worktree 建立后主仓 HEAD 是否已经前进(`Worktree.base` 字段此前\"apply 时检漂移\"\n * 的注释所指,但从未被消费——apply 只靠 `git apply --check` 判定能否干净应用)。这是一个\n * **提示性**信号(`applyDiff` 的 `--check` 才是真正的冲突判定),用于向用户解释\"为什么这个\n * apply 可能出乎意料地冲突/无冲突\",不用来拒绝 apply。\n */\nexport function hasRepoDrifted(repoRoot: string, base: string): boolean {\n try {\n return git(repoRoot, ['rev-parse', 'HEAD']) !== base\n } catch {\n // 无法判定(例如 repoRoot 已不是有效 git 仓)——保守地不报漂移,交给 apply --check 定夺。\n return false\n }\n}\n\n/**\n * 把作业 diff apply 到主仓工作区。**check-first,不强改**:先 `git apply --check` 验证能否干净\n * 应用——能才真 apply;不能(主仓漂移/冲突)则返回 applied=false + conflictPaths,**绝不**往主树\n * 写冲突标记(区别于 `--3way` 会留 `>>>>>>>` 撕裂工作区)。让用户在 worktree 解决后重试或 cancel。\n */\nexport function applyDiff(repoRoot: string, diff: string): ApplyDiffResult {\n if (!diff.trim()) return { applied: true, conflictPaths: [] }\n try {\n execFileSync('git', ['-C', repoRoot, 'apply', '--check', '--whitespace=nowarn'], {\n input: diff,\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n } catch (e) {\n return {\n applied: false,\n conflictPaths: parseConflictPaths(String((e as { stderr?: unknown }).stderr ?? '')),\n }\n }\n try {\n execFileSync('git', ['-C', repoRoot, 'apply', '--whitespace=nowarn'], {\n input: diff,\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n return { applied: true, conflictPaths: [] }\n } catch (e) {\n return {\n applied: false,\n conflictPaths: parseConflictPaths(String((e as { stderr?: unknown }).stderr ?? '')),\n }\n }\n}\n\n/**\n * 把已 apply 的 diff 从主仓工作区**反向撤销**(`git apply --reverse`)。RFC-026 §4.2 回退。\n * 同样 **check-first,不强改**:工作树自 apply 后漂移(用户又改了那些文件)→ reverse check 失败 →\n * 返回 applied=false + conflictPaths,绝不强撤。干净才真撤。\n */\nexport function revertDiff(repoRoot: string, diff: string): ApplyDiffResult {\n if (!diff.trim()) return { applied: true, conflictPaths: [] }\n try {\n execFileSync('git', ['-C', repoRoot, 'apply', '--reverse', '--check', '--whitespace=nowarn'], {\n input: diff,\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n } catch (e) {\n return {\n applied: false,\n conflictPaths: parseConflictPaths(String((e as { stderr?: unknown }).stderr ?? '')),\n }\n }\n try {\n execFileSync('git', ['-C', repoRoot, 'apply', '--reverse', '--whitespace=nowarn'], {\n input: diff,\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n return { applied: true, conflictPaths: [] }\n } catch (e) {\n return {\n applied: false,\n conflictPaths: parseConflictPaths(String((e as { stderr?: unknown }).stderr ?? '')),\n }\n }\n}\n\n/**\n * 移除 worktree + 删临时分支(幂等 best-effort:已不在/已删不报错)。\n *\n * review J4:三步仍各自 best-effort(单步失败不阻塞后续——分支已被 prune 掉的场景下\n * `branch -D` 失败是正常噪音),但**不再完全静默**——失败会经可选 `onError` 报告,调用方\n * 可接 logger 观测到\"清理不彻底\"(此前三个空 catch 让权限/NFS stale handle/进程占用等\n * 真实失败无声吞掉,运维完全无感知)。`onError` 缺省不传时行为与修复前一致(纯静默)。\n */\nexport function removeWorktree(\n wt: Worktree,\n onError?: (op: 'worktree remove' | 'branch delete' | 'worktree prune', err: unknown) => void,\n): void {\n try {\n git(wt.repoRoot, ['worktree', 'remove', '--force', wt.path])\n } catch (e) {\n onError?.('worktree remove', e)\n }\n try {\n git(wt.repoRoot, ['branch', '-D', wt.branch])\n } catch (e) {\n onError?.('branch delete', e)\n }\n try {\n git(wt.repoRoot, ['worktree', 'prune'])\n } catch (e) {\n onError?.('worktree prune', e)\n }\n}\n\n/**\n * 扫描并清理孤儿 worktree(SIGKILL/OOM 未走 exit reaper 留下的残留)。\n * 用 `git -C repoRoot worktree list --porcelain` 列出该仓的全部已注册 worktree,\n * 筛选出:① 路径匹配 tmpdir 下 `otto-wt-*` 模式的(本系统创建的),\n * ② 分支名匹配 `otto/job-*` 的,\n * 且这些 worktree 不在 `activeJobIds`(当前存活作业 id 集合,调用方传入,避免误删正在跑的)里对应的路径中。\n * 对匹配到的孤儿逐个 `git worktree remove --force` + `git branch -D`,\n * 最后 `git worktree prune`。全程 best-effort(单条失败不中断其余)。\n * 返回被清理的路径列表(供日志/测试断言)。\n */\nexport function pruneOrphanWorktrees(repoRoot: string, activeJobIds: Iterable<string>): string[] {\n const activeSet = new Set(activeJobIds)\n const cleaned: string[] = []\n\n // macOS: tmpdir() → /var/folders/… 但 git 输出是 /private/var/folders/…(/var→/private/var 符号链接)。\n // realpathSync 归一化 tmpdir() 自身(它真实存在)再拼 otto-wt- 前缀。\n let wtPrefix: string\n try {\n wtPrefix = join(realpathSync(tmpdir()), 'otto-wt-')\n } catch {\n return cleaned\n }\n\n let output: string\n try {\n output = git(repoRoot, ['worktree', 'list', '--porcelain'])\n } catch {\n return cleaned\n }\n\n // porcelain 输出:每个 worktree block 用空行分隔。提取 worktree <path> 和 branch refs/heads/<name> 行。\n const blocks = output.split('\\n\\n')\n for (const block of blocks) {\n const lines = block.split('\\n')\n let worktreePath = ''\n let branchName = ''\n\n for (const line of lines) {\n if (line.startsWith('worktree ')) {\n worktreePath = line.slice('worktree '.length)\n } else if (line.startsWith('branch ')) {\n branchName = line.slice('branch '.length)\n }\n }\n\n if (!worktreePath || !branchName) continue\n\n // 筛选:branch 匹配 refs/heads/otto/job-<jobId>\n const branchMatch = /^refs\\/heads\\/otto\\/job-(.+)$/.exec(branchName)\n if (!branchMatch) continue\n\n // noUncheckedIndexedAccess 下数组索引返回 string|undefined;已知 regex (.+) 必有捕获故断言。\n const jobId = branchMatch[1]!\n\n // 双重确认:路径匹配本系统 tmpdir 前缀(归一化后比对,规避 macOS /var vs /private/var 漂移)\n let resolvedPath: string\n try {\n resolvedPath = realpathSync(worktreePath)\n } catch {\n // worktree 目录已不存在(可能被外部清理),跳过即可\n continue\n }\n if (!resolvedPath.startsWith(wtPrefix)) continue\n\n // 跳过活跃作业\n if (activeSet.has(jobId)) continue\n\n // 孤儿:清理\n try {\n git(repoRoot, ['worktree', 'remove', '--force', worktreePath])\n } catch {}\n try {\n const shortBranch = branchName.slice('refs/heads/'.length)\n git(repoRoot, ['branch', '-D', shortBranch])\n } catch {}\n cleaned.push(worktreePath)\n }\n\n // 最后统一 prune\n try {\n git(repoRoot, ['worktree', 'prune'])\n } catch {}\n\n return cleaned\n}\n","/**\n * forbidden-zone.ts —— RFC-026 §4.0 治理禁区锁(自我进化的前提铁律)。\n *\n * 自我修改系统最大的风险是**改写绑住自己的绳子**:一个 delegate 的 diff 里夹带改自主分级、\n * 出网 allowlist、权限/沙箱、apply/revert 能力、设置 schema……于是任何\"自主级\"最终都能解除\n * 约束自己的规则。本模块提供一个**独立守卫**:扫 diff 触及的路径 ∩ 禁区集合 → 命中即\"高危\"。\n *\n * 用法(分级):\n * - 手动 apply(L0,人即闸):命中只**高危警示**,仍可应用(人已在审)。\n * - 未来自主层(L≥2 自动 apply):命中**强制降 L0、拒绝自动落地**(不可被任何自主级旁路)。\n *\n * **自含**:禁区清单包含本文件自身(`governance/forbidden-zone`)——改禁区清单的 diff 自身即被\n * 标记,故无法被悄悄缩小/移除。终局还应把清单**外置/签名**(本阶段先在引擎内 + 自含兜底)。\n */\n\n/** 禁区路径模式(匹配 repo 相对路径)。保守从宽——宁可多标人审,不可漏放(RFC §4.0)。 */\nexport const FORBIDDEN_ZONE_PATTERNS: readonly RegExp[] = [\n /(^|\\/)governance\\/forbidden-zone\\./,\n /(^|\\/)packages\\/setting\\//,\n /(^|\\/)permission/i,\n /(^|\\/)sandbox/i,\n /(^|\\/)git-worktree\\./,\n /(^|\\/)agent-job-service\\./,\n /(^|\\/)mcp-controller\\./,\n /(^|\\/)packages\\/mcp\\/src\\/oauth\\//,\n /(allowlist|allow-list|egress|networkpolicy)/i,\n]\n\n/**\n * 还原 git quotepath 引号化路径:含特殊字符/非 ASCII 时 git 用 \"...\" 包裹并八进制转义(\\NNN)\n * + C 转义(\\t \\n \\r \\\\ \\\")。不还原则治理守卫对这些文件名 fail-open(特殊字符文件名绕过禁区)。\n */\nfunction unquoteGitPath(token: string): string {\n if (token.length < 2 || !token.startsWith('\"') || !token.endsWith('\"')) return token\n const inner = token.slice(1, -1)\n const bytes: number[] = []\n for (let i = 0; i < inner.length; i++) {\n if (inner[i] !== '\\\\') {\n bytes.push(inner.charCodeAt(i))\n continue\n }\n const next = inner[i + 1]\n if (next && next >= '0' && next <= '7') {\n bytes.push(parseInt(inner.slice(i + 1, i + 4), 8))\n i += 3\n } else {\n const esc: Record<string, number> = { t: 9, n: 10, r: 13, '\"': 34, '\\\\': 92 }\n bytes.push(esc[next!] ?? (next ? next.charCodeAt(0) : 92))\n i += 1\n }\n }\n return Buffer.from(bytes).toString('utf-8')\n}\n\n/** 还原 quotepath 后剥 a//b/ 前缀;/dev/null → null。 */\nfunction stripDiffPath(token: string): string | null {\n const unq = unquoteGitPath(token.trim())\n if (unq === '/dev/null') return null\n return unq.replace(/^[ab]\\//, '')\n}\n\n/** 拆 `diff --git` 行的两个路径 token:引号化 `\"a/x\" \"b/x\"` 或裸 `a/x b/x`。 */\nfunction splitDiffGitPaths(rest: string): string[] {\n const quoted = /^(\"(?:[^\"\\\\]|\\\\.)*\") (\"(?:[^\"\\\\]|\\\\.)*\")$/.exec(rest)\n if (quoted) return [quoted[1]!, quoted[2]!]\n const m = /^(a\\/.+?) (b\\/.+)$/.exec(rest)\n return m ? [m[1]!, m[2]!] : []\n}\n\n/**\n * 从统一 diff 里提取触及的文件路径(repo 相对)。覆盖 diff --git / +++ / --- 行;跳过 /dev/null。\n * 还原 git quotepath(引号化/八进制转义)——否则特殊字符文件名绕过治理守卫(fail-open)。\n */\nexport function diffTouchedPaths(diff: string): string[] {\n const paths = new Set<string>()\n for (const line of diff.split('\\n')) {\n if (line.startsWith('diff --git ')) {\n for (const tok of splitDiffGitPaths(line.slice('diff --git '.length))) {\n const p = stripDiffPath(tok)\n if (p) paths.add(p)\n }\n continue\n }\n // 限定文件头(+++/--- 后接 a//b/ 或引号包裹),避免误吃 `++ x`/`-- x` 内容行。\n if (/^\\+\\+\\+ \"?[ab]\\//.test(line) || /^--- \"?[ab]\\//.test(line)) {\n const p = stripDiffPath(line.slice(4))\n if (p) paths.add(p)\n }\n }\n return [...paths]\n}\n\n/**\n * 某 diff 触及的**禁区文件**(空数组=未触禁区)。调用方据此:手动 apply 高危警示 / 自主层强制 L0。\n */\nexport function touchesForbiddenZone(diff: string): string[] {\n if (!diff.trim()) return []\n return diffTouchedPaths(diff).filter((p) => FORBIDDEN_ZONE_PATTERNS.some((re) => re.test(p)))\n}\n","/**\n * capability-surfaces.ts —— RFC-026 阶段2:识别一个 diff 是否触及\"能力面\"。\n *\n * delegate 给 otto 加能力(MCP / provider / 声明式 agent·skill·team)= 改项目级配置/声明文件。\n * apply 后若 diff 触及能力面 → 自动热重载(reloadCapabilities),不重启即认到新能力。\n *\n * delegate 的 diff 只能改 worktree(repo)内文件,故能力新增必落在项目级 `.otto/`(agents/skills/\n * teams/config)或代码里——全局 `~/.otto/` 不在 worktree 范围。这里只认项目级配置/声明面。\n */\n\nimport { diffTouchedPaths } from './forbidden-zone'\n\n/** 能力面路径模式(匹配 repo 相对路径)。命中即该 diff 改动了可热重载的能力配置/声明。 */\nexport const CAPABILITY_SURFACE_PATTERNS: readonly RegExp[] = [\n /(^|\\/)\\.otto\\/(agents|skills|teams)\\//i,\n /(^|\\/)\\.otto\\/[^/]*config/i,\n /(^|\\/)otto\\.config\\./i,\n]\n\n/** 某 diff 触及的**能力面文件**(空数组=未触;非空=apply 后应热重载)。 */\nexport function touchesCapabilitySurface(diff: string): string[] {\n if (!diff.trim()) return []\n return diffTouchedPaths(diff).filter((p) => CAPABILITY_SURFACE_PATTERNS.some((re) => re.test(p)))\n}\n","/**\n * apply-policy.ts —— 自主 apply 策略层的**纯决策核**。\n *\n * 自我进化闭环里那道「闸」:一个 delegate 作业产出的 diff 要不要自动落主仓。本模块只回答\n * \"怎么落\"(自动 / 建议人审 / 强制人审 / 禁区拒绝),**不执行任何 apply**——执行由调用方\n * (agent-job-service.apply 回路)据 Verdict 决定。纯函数:无 IO、无副作用,全 L0–L4 × 信号\n * 矩阵可单测覆盖。\n *\n * 铁律(第一判定,不可旁路):\n * origin='main'(自主)+ 触禁区 → 永远 forbid,不存在任何 autonomy 级/配置能让它自动落地。\n * 禁区清单见 governance/forbidden-zone(自含:改禁区清单的 diff 自身即触禁区)。\n *\n * 人手路径(origin='user'):人即闸——只产建议(advise),从不 forbid/auto。\n */\n\n/** 自主分级。默认 0 = 人即闸 = 现状,零行为变化。 */\nexport type AutonomyLevel = 0 | 1 | 2 | 3 | 4\n\n/** decideApply 的输入信号——全部由调用方计算后注入(保持本函数纯粹)。 */\nexport interface ApplySignals {\n /** touchesForbiddenZone(diff) 的结果(空=未触禁区)。 */\n forbiddenZone: string[]\n /** touchesCapabilitySurface(diff) 的结果(空=未触能力面)。 */\n capabilityTouched: string[]\n /** diff 规模(增/删行)。 */\n diffStat: { added: number; removed: number }\n /** applyDiff --check 预检:true=有冲突,不可干净落地。 */\n conflict: boolean\n /** 发起方:user=人手 /job apply;main=模型自主作业;plugin=插件代码自主发起(RFC-287 R2)。 */\n origin: 'user' | 'main' | 'plugin'\n}\n\nexport interface ApplyPolicy {\n level: AutonomyLevel\n /** 自动落地的 diff 规模上限(增+删行);超过则降人手。默认 DEFAULT_MAX_AUTO_APPLY_LINES。 */\n maxAutoApplyLines?: number\n}\n\nexport type Verdict =\n /** 自动落地(仅 origin='main' + level≥2 + 全部低风险条件满足)。 */\n | { action: 'auto' }\n /** 人手落地 + 推荐(人即闸或 L1)。recommend=apply/review。 */\n | { action: 'advise'; recommend: 'apply' | 'review'; reasons: string[] }\n /** 强制人手(自主但被降级:L0 / 冲突 / 超规模 / 能力面越级)。 */\n | { action: 'gate'; reasons: string[] }\n /** 禁区命中:拒绝自动落地(铁律,自主路径专属)。 */\n | { action: 'forbid'; reasons: string[] }\n\n/** 自动落地默认规模上限(保守)。超过即便低风险也降人手——大改动人审。 */\nexport const DEFAULT_MAX_AUTO_APPLY_LINES = 200\n\n/**\n * 纯决策:据信号 + 策略产出 Verdict。无副作用。\n *\n * 判定顺序(铁律优先):\n * 1. origin='main'|'plugin' + 触禁区 → forbid(第一判定,任何级别不可旁路)。\n * 2. origin='plugin' → 永不自动落地(RFC-287 R2):触禁区已被上一条 forbid,其余一律\n * gate(强制人手)。插件代码自主发起的作业不享受 user 的\"人即闸 advise\"待遇(那条\n * 分支从不 forbid),也不参与 main 的 autonomy 自动落地——独立且最保守的一档。\n * 3. origin='user' → advise(人即闸;有风险信号则 recommend=review,否则 apply)。\n * 4. origin='main' + level≤1 → 不自动:L0=gate(必须人手),L1=advise(带推荐)。\n * 5. origin='main' + level≥2 → 低风险则 auto;否则 gate(列出 blocker)。\n * 低风险 = 无冲突 ∧ 规模≤上限 ∧(L2:未触能力面 / L3+:允许能力面)。\n */\nexport function decideApply(signals: ApplySignals, policy: ApplyPolicy): Verdict {\n const { forbiddenZone, capabilityTouched, diffStat, conflict, origin } = signals\n const maxLines = policy.maxAutoApplyLines ?? DEFAULT_MAX_AUTO_APPLY_LINES\n const totalLines = diffStat.added + diffStat.removed\n\n if (origin !== 'user' && forbiddenZone.length > 0) {\n return {\n action: 'forbid',\n reasons: [`触及治理禁区(高危自我修改):${forbiddenZone.join(', ')}`],\n }\n }\n\n // RFC-287 R2:插件自主发起的作业永不自动落地,且不走 user 的 advise 分支。\n // 放在 user/main 分流之前——避免未来新增分支时被\"默认归入某一档\"悄悄放宽。\n if (origin === 'plugin') {\n const reasons: string[] = ['插件发起的作业需人手 apply(RFC-287 R2)']\n if (capabilityTouched.length) reasons.push(`触及能力面:${capabilityTouched.join(', ')}`)\n if (conflict) reasons.push('存在冲突')\n if (totalLines > maxLines) reasons.push(`改动较大(${totalLines} 行 > ${maxLines})`)\n return { action: 'gate', reasons }\n }\n\n if (origin === 'user') {\n const reasons: string[] = []\n if (forbiddenZone.length) reasons.push(`触及治理禁区:${forbiddenZone.join(', ')}`)\n if (capabilityTouched.length) reasons.push(`触及能力面:${capabilityTouched.join(', ')}`)\n if (conflict) reasons.push('存在冲突,无法干净落地')\n return { action: 'advise', recommend: reasons.length ? 'review' : 'apply', reasons }\n }\n\n if (policy.level <= 1) {\n const reasons: string[] = []\n if (capabilityTouched.length) reasons.push(`触及能力面:${capabilityTouched.join(', ')}`)\n if (conflict) reasons.push('存在冲突')\n if (totalLines > maxLines) reasons.push(`改动较大(${totalLines} 行 > ${maxLines})`)\n if (policy.level === 0) {\n return { action: 'gate', reasons: ['autonomy L0:需人手 apply', ...reasons] }\n }\n return { action: 'advise', recommend: reasons.length ? 'review' : 'apply', reasons }\n }\n\n const blockers: string[] = []\n if (conflict) blockers.push('apply 有冲突')\n if (totalLines > maxLines) blockers.push(`改动超规模(${totalLines} 行 > ${maxLines})`)\n if (capabilityTouched.length && policy.level < 3) {\n blockers.push(`触及能力面(需 L3+):${capabilityTouched.join(', ')}`)\n }\n if (blockers.length) return { action: 'gate', reasons: blockers }\n return { action: 'auto' }\n}\n","/**\n * agent-job-registry.ts —— RFC-023 Phase 1:agent-session 作业注册表。\n *\n * 照 ProcessRegistry 同款形状(register/get/list/listForSession/onSpawn/onExit + 全局单例 +\n * 宿主 reaper),但更有状态:作业经 running → ready(产 diff) → applied/failed/canceled 流转,\n * 故多一个 update + onUpdate。cancel = abort detached run + removeWorktree。\n *\n * **先并列不抽象**(RFC 决策):与 ProcessRegistry 并存,待第 3 个 kind 出现再提炼 TaskFramework。\n */\n\nimport { removeWorktree, pruneOrphanWorktrees, type Worktree } from './git-worktree'\n\nexport type AgentJobStatus = 'running' | 'needs_input' | 'ready' | 'applied' | 'failed' | 'canceled'\n\nconst TERMINAL: ReadonlySet<AgentJobStatus> = new Set(['applied', 'failed', 'canceled'])\n\nconst DEFAULT_MAX_JOBS = 16\n\n/**\n * 作业来源(RFC-026):user = 用户 /delegate 委派的;main = 主对话(模型)在答复过程中起的。\n *\n * RFC-287 D2/R2 新增 `plugin`:插件代码经 `PluginHostCapabilities.startJob` 自主发起的作业\n * (需高危能力 `agent.dispatch`)。**必须是独立取值、不可复用 user/main**——`decideApply`\n * (governance/apply-policy)按 origin 分流:`'user'` 视为\"人即闸\"从不 forbid、`'main'` 在\n * 高 autonomy 下可 auto 落地,两者都不适用于\"插件代码自主发起\"这一档信任级别。独立取值\n * 使插件作业在策略层永远走最保守分支(见 apply-policy `decideApply`)。\n */\nexport type JobOrigin = 'user' | 'main' | 'plugin'\n\nexport interface AgentSessionJob {\n id: string\n sessionId: string\n type: 'agent-session'\n title: string\n /** 来源:用户委派 vs 主对话委派(面板分组用)。 */\n origin: JobOrigin\n status: AgentJobStatus\n /** 隔离 worktree(产物所在)。 */\n worktree: Worktree\n /** 转录输出文件(logPath 式;/jobs tail + 完成通知指针)。 */\n outputPath: string\n startedAt: number\n /** ready 后填:worktree vs base 的统一 diff。 */\n diff?: string\n /** failed 时填。 */\n error?: string\n}\n\nexport interface RegisterAgentJobInput {\n id: string\n sessionId: string\n title: string\n worktree: Worktree\n outputPath: string\n /** 来源(缺省 user)。 */\n origin?: JobOrigin\n /** 取消该作业的 detached run(spawnDetachedRun 返回的 abort)。 */\n abort: () => void\n now?: number\n}\n\ninterface Entry {\n info: AgentSessionJob\n abort: () => void\n}\n\nexport type AgentJobListener = (job: AgentSessionJob) => void\n\nexport interface AgentJobRegistryOptions {\n maxJobs?: number\n clock?: () => number\n /**\n * worktree 清理失败上报(review J4)。缺省不传时 `removeWorktree` 的三步失败仍完全静默\n * (行为等同修复前)——传入后可接 logger 观测权限/NFS stale handle/进程占用等真实失败。\n */\n onWorktreeCleanupError?: (\n jobId: string,\n op: 'worktree remove' | 'branch delete' | 'worktree prune',\n err: unknown,\n ) => void\n}\n\nexport class AgentJobRegistry {\n private readonly entries = new Map<string, Entry>()\n private readonly maxJobs: number\n private readonly clock: () => number\n private onWorktreeCleanupError?: AgentJobRegistryOptions['onWorktreeCleanupError']\n private readonly spawnListeners = new Set<AgentJobListener>()\n private readonly updateListeners = new Set<AgentJobListener>()\n private readonly exitListeners = new Set<AgentJobListener>()\n private readonly removeListeners = new Set<AgentJobListener>()\n private readonly retained = new Set<string>()\n\n constructor(options: AgentJobRegistryOptions = {}) {\n this.maxJobs = options.maxJobs ?? DEFAULT_MAX_JOBS\n this.clock = options.clock ?? (() => Date.now())\n this.onWorktreeCleanupError = options.onWorktreeCleanupError\n }\n\n /**\n * 补设 worktree 清理失败上报(review J4)。`globalAgentJobRegistry` 是模块级单例,构造时\n * 无法从 app 层注入 logger——app 装配阶段调用本方法一次即可后接。\n */\n setWorktreeCleanupErrorHandler(handler: AgentJobRegistryOptions['onWorktreeCleanupError']): void {\n this.onWorktreeCleanupError = handler\n }\n\n onSpawn(listener: AgentJobListener): () => void {\n this.spawnListeners.add(listener)\n return () => this.spawnListeners.delete(listener)\n }\n\n /** 状态/diff 变更(running→ready/needs_input 等)。 */\n onUpdate(listener: AgentJobListener): () => void {\n this.updateListeners.add(listener)\n return () => this.updateListeners.delete(listener)\n }\n\n /** 终态(applied/failed/canceled)通知一次。 */\n onExit(listener: AgentJobListener): () => void {\n this.exitListeners.add(listener)\n return () => this.exitListeners.delete(listener)\n }\n\n /** 作业从表中移除(手动删除/容量驱逐后)。cli 据此重推 /jobs(作业消失)。 */\n onRemove(listener: AgentJobListener): () => void {\n this.removeListeners.add(listener)\n return () => this.removeListeners.delete(listener)\n }\n\n /**\n * R4:查看某作业时 retain;停止查看时 off。\n *\n * 2026-07-30 语义变更(applied 后 3 秒条目蒸发的终局修复):终态作业**不再定时驱逐**,\n * 保留至用户手动 `d` 删除(onBackgroundDelete → remove)或容量压力驱逐(register 满员\n * 时 evictTerminal)。retain 从\"阻断驱逐定时器\"改为\"容量驱逐保护\"——正在被查看的\n * 终态作业不会被 evictTerminal 腾退。对照社区(Claude Code / Roo Code):终态任务历史\n * 持续可见;原 3s graceMs 系对 CC STOPPED_DISPLAY_MS 的语义误用(那是 UI 显示宽限,\n * 不是数据条目驱逐宽限)。\n */\n retain(id: string, on: boolean): void {\n if (on) {\n this.retained.add(id)\n } else {\n this.retained.delete(id)\n }\n }\n\n private notify(listeners: Set<AgentJobListener>, info: AgentSessionJob): void {\n for (const l of [...listeners]) {\n try {\n l(info)\n } catch {}\n }\n }\n\n /**\n * 登记作业。撞满上限时先驱逐已终态的;若全在跑则抛错(提示先 cancel)。\n * **不**驱逐在跑的 agent 作业(不像进程 LRU——杀活作业会丢工作)。\n */\n register(input: RegisterAgentJobInput): AgentSessionJob {\n if (this.entries.size >= this.maxJobs) {\n this.evictTerminal()\n }\n if (this.entries.size >= this.maxJobs) {\n throw new Error(`Agent job limit (${this.maxJobs}) reached; cancel one with stop first.`)\n }\n const now = input.now ?? this.clock()\n const info: AgentSessionJob = {\n id: input.id,\n sessionId: input.sessionId,\n type: 'agent-session',\n title: input.title,\n origin: input.origin ?? 'user',\n status: 'running',\n worktree: input.worktree,\n outputPath: input.outputPath,\n startedAt: now,\n }\n this.entries.set(input.id, { info, abort: input.abort })\n this.notify(this.spawnListeners, info)\n return info\n }\n\n get(id: string): AgentSessionJob | undefined {\n return this.entries.get(id)?.info\n }\n\n getForSession(sessionId: string, id: string): AgentSessionJob | undefined {\n const e = this.entries.get(id)\n return e && e.info.sessionId === sessionId ? e.info : undefined\n }\n\n list(): AgentSessionJob[] {\n return [...this.entries.values()].map((e) => e.info)\n }\n\n listForSession(sessionId: string): AgentSessionJob[] {\n return this.list().filter((j) => j.sessionId === sessionId)\n }\n\n size(): number {\n return this.entries.size\n }\n\n /**\n * 更新作业状态/diff/error(running→ready 等)。终态(applied/failed/canceled)额外触发 onExit\n * 并清理 worktree(applied/failed;canceled 由 cancel() 自己清)。\n */\n update(id: string, patch: Partial<Pick<AgentSessionJob, 'status' | 'diff' | 'error'>>): void {\n const e = this.entries.get(id)\n if (!e) return\n if (TERMINAL.has(e.info.status)) return\n Object.assign(e.info, patch)\n this.notify(this.updateListeners, e.info)\n if (TERMINAL.has(e.info.status)) {\n if (e.info.status !== 'canceled') {\n removeWorktree(e.info.worktree, (op, err) => this.onWorktreeCleanupError?.(id, op, err))\n }\n this.notify(this.exitListeners, e.info)\n }\n }\n\n /**\n * 取消作业:abort detached run + removeWorktree + status=canceled(onExit 一次)。\n *\n * review J5:改走 `update()` 流转状态(而非直改 `e.info.status` + 手动发 onExit),使\n * canceled 与 applied/failed 一样触发 onUpdate——此前 onUpdate 订阅者(如需据状态变化刷新\n * UI 的消费者)会漏收 canceled 事件。worktree 仍在此处**先行**清理(update() 对已是\n * 'canceled' 的状态会跳过它自己的 removeWorktree,避免重复清理)。\n */\n cancel(id: string): void {\n const e = this.entries.get(id)\n if (!e || TERMINAL.has(e.info.status)) return\n try {\n e.abort()\n } catch {}\n removeWorktree(e.info.worktree, (op, err) => this.onWorktreeCleanupError?.(id, op, err))\n this.update(id, { status: 'canceled' })\n }\n\n /**\n * 从表中移除(用户 `d` 手动删除 / 容量驱逐后调用)。清 retain 并通知 onRemove。\n * 防御性兜底:再调一次 removeWorktree(review P1:正常终态流转在 update()/cancel() 已清理过,\n * 这里是\"清理被跳过\"异常路径的兜底——removeWorktree 内部 try/catch 逐步执行,对已清理/\n * 不存在的 worktree 是安全空操作,不会因重复调用报错或产生副作用)。\n */\n remove(id: string): void {\n const e = this.entries.get(id)\n if (e) removeWorktree(e.info.worktree, (op, err) => this.onWorktreeCleanupError?.(id, op, err))\n this.retained.delete(id)\n this.entries.delete(id)\n if (e) this.notify(this.removeListeners, e.info)\n }\n\n /**\n * 撞上限时驱逐已终态作业(不动在跑的;跳过 retained——正被查看的终态作业不腾退)。\n * 走 remove 以发 onRemove。终态不再定时驱逐后,这里是唯一的自动驱逐路径(容量压力)。\n */\n private evictTerminal(): void {\n for (const [id, e] of this.entries) {\n if (TERMINAL.has(e.info.status) && !this.retained.has(id)) {\n this.remove(id)\n return\n }\n }\n }\n\n /** 宿主退出兜底:同步 cancel 所有在跑作业(abort + removeWorktree),防 worktree 泄漏。 */\n cleanupAll(): void {\n for (const [id, e] of this.entries) {\n if (!TERMINAL.has(e.info.status)) {\n try {\n e.abort()\n } catch {}\n removeWorktree(e.info.worktree, (op, err) => this.onWorktreeCleanupError?.(id, op, err))\n e.info.status = 'canceled'\n }\n }\n }\n}\n\n/** 进程级单例:宿主与作业服务共享。 */\nexport const globalAgentJobRegistry = new AgentJobRegistry()\n\nlet reaperInstalled = false\n\n/** 安装宿主退出兜底:process.on('exit') → cleanupAll(清 worktree)。幂等。 */\nexport function installAgentJobReaper(\n registry: AgentJobRegistry = globalAgentJobRegistry,\n repoRoot?: string,\n): void {\n if (reaperInstalled) return\n reaperInstalled = true\n\n // 启动时先清一遍 SIGKILL/OOM 残留的孤儿 worktree\n if (repoRoot) {\n const activeJobIds = registry.list().map((j) => j.id)\n pruneOrphanWorktrees(repoRoot, activeJobIds)\n }\n\n process.on('exit', () => {\n registry.cleanupAll()\n })\n}\n","/**\n * Agent 观测层(子代理透明化的地基)。\n *\n * 子代理是裸 `@x-otto/agent` Agent(swarm-runtime / detached-run),其 stream/tool/turn 事件刻意不接主 TUI\n * (转录隔离)。本模块按**观测 id** 收集每个 Agent 的运行态(turn/工具/token/最后事件时刻 + 阻塞类别),\n * 供 Fleet 面板列出 + 视图B 诊断卡死。设计照 ProcessRegistry/AgentJobRegistry 同款(全局单例 + 订阅 +\n * 不阻塞退出的驱逐定时器)。\n *\n * 关键取舍:\n * - **stall 是派生量**——快照读取时用 `now - lastEventAt` 计算,不存储;高频 stream delta 只更新\n * `lastEventAt` 不触发 notify(避免每 token 一次 UI 推送风暴),由消费方的动画 tick 重读重算。\n * - **结构性事件**(turn/tool/status 转换)才 notify。\n */\nimport type { Agent, AgentEvent, AgentMessage, AgentStatus } from '@x-otto/agent'\nimport { TypedEventEmitter, type Events } from '@x-otto/shared'\n\n/**\n * 观测态:比引擎 AgentStatus 多两个派生态——`awaiting_gate`(等审批)、\n * `tool_waiting`(RFC-144 M1:有 lifecycleAsync 工具在后台执行,loop 未阻塞但也非常规\n * streaming/tool_executing,供宿主面板区分\"模型在想\"与\"后台任务在跑,模型可继续其他工作\")。\n *\n * `tool_waiting` 是接口占位(M1 设计,M3 才由 workLoop 控制流真正驱动状态转换)——\n * 当前无生产者调用 `setToolWaiting`/`clearToolWaiting`(下方新增方法),这是刻意的:\n * M1 的范围是\"pending 追踪\"而非\"控制流改造\"(后者需要 runTurnsUntilTerminate 在\n * post-runTools 检查 pending 非空后 yield,这是 M3 的工作)。\n */\nexport type AgentObsStatus =\n | 'streaming'\n | 'tool_executing'\n | 'awaiting_gate'\n | 'tool_waiting'\n | 'done'\n | 'error'\n | 'aborted'\n\n/** 阻塞类别——映射「四类无界等待」中子代理可见的三类(外部进程超时化为 D 类 backlog)。 */\nexport type BlockClass = 'model_io' | 'approval' | 'process'\n\n// RFC-074 D7:TurnRecord 单一真源下沉 @x-otto/session-contract(前端无关契约),runtime 与 tui\n// 共引一处,消除\"runtime TurnRecord ⟂ tui TurnLike\"手抄镜像。re-export 保持 barrel 兼容。\nexport type { TurnRecord } from '@x-otto/session-contract'\nimport type { TurnRecord } from '@x-otto/session-contract'\n\n/** 一个被观测 Agent 的累积运行态(不含派生量)。 */\nexport interface AgentObservation {\n /** 唯一观测键(swarm=`${runId}:${memberId}`,detached=runId)。 */\n id: string\n agentName: string\n title?: string\n /** 完整的委托提示词(供 SubagentsPane / 详情面板展示)。 */\n prompt?: string\n origin: 'user' | 'main'\n depth: number\n status: AgentObsStatus\n turnIndex: number\n toolUseCount: number\n /** 累积文件编辑次数(edit/write/edit_file/write_file 工具调用)。 */\n filesEdited: number\n /** 累积输出 token(逐 turn.end 累加)。 */\n tokenCount: number\n inputTokens: number\n /** 分配模型 id(供 SubagentsPane 面板显示;缺省空串)。 */\n model?: string\n /** 模型决策层(explicit/slot/category/default/fallback)——面板显示「为何用这个模型」。 */\n modelDecidedBy?: string\n /** 人类可读的当前活动:thinking / responding / using <tool> / reconnecting / done。 */\n lastActivity: string\n /** 最近一次事件时刻(含高频 stream delta)——stall 判定基准。 */\n lastEventAt: number\n /** 当前等待类别(仅在「停住」时有诊断意义;配合 stall 级别看)。 */\n blockedOn?: BlockClass\n blockDetail?: string\n /** 父 abort 能否中断它(fork_call 修复前曾为 false;现恒 true,保留字段以暴露未来回归)。 */\n abortable: boolean\n startedAt: number\n endedAt?: number\n}\n\n/** 快照 = 观测态 + 派生量(idleMs / stall 级别)。 */\nexport interface AgentObservationSnapshot extends AgentObservation {\n /** 距上次事件毫秒(已结束则 0)。 */\n idleMs: number\n /** stall 级别:none / warn(>warnMs)/ stalled(>stalledMs);已结束恒 none。 */\n stall: 'none' | 'warn' | 'stalled'\n}\n\nexport interface AgentObservationMeta {\n id: string\n agentName: string\n title?: string\n /** 完整的委托提示词(供 SubagentsPane / 详情面板展示)。 */\n prompt?: string\n /** 分配模型 id(供 SubagentsPane 面板显示)。 */\n model?: string\n /** 模型决策层(供面板显示选型溯源)。 */\n modelDecidedBy?: string\n origin?: 'user' | 'main'\n depth?: number\n /** 中止该 agent 的回调(swarm=agent.abort;detached=清 pending approval + agent.abort)。\n * 提供即 `abortable=true`;面板 `x`/`k` 经 `registry.abort(id)` 调用它中止卡死的子代理。 */\n abort?: () => void\n /** 显式覆盖 abortable(缺省由 abort 是否提供推断)。 */\n abortable?: boolean\n /** 视图A:子代理的 live 转录数组引用(=AgentRunContext.messages,agent 原地 push)。\n * registry 只持引用、读取时取快照;供「对话」视图渲染。不进 list() 轻量快照(按需 getMessages)。\n * 可传**取值函数**——AgentSession 子代理(task_delegate/fork 等)无稳定 live 数组,\n * 传 `() => session.session.messages()` 让 getMessages 每次取最新(task-runner finally 再 setMessages 冻结终态)。 */\n messages?: readonly AgentMessage[] | (() => readonly AgentMessage[])\n}\n\nexport interface AgentObservabilityOptions {\n now?: () => number\n /** stall 黄阈(默认 3s)。 */\n warnMs?: number\n /** stall 红阈(默认 30s)。 */\n stalledMs?: number\n /** 结束后保留多久再驱逐(默认 15s)。 */\n evictMs?: number\n}\n\ntype ObservabilityEvents = Events & { change: () => void }\n\nconst TERMINAL: ReadonlySet<AgentObsStatus> = new Set(['done', 'error', 'aborted'])\n\nconst FILE_EDIT_TOOLS: ReadonlySet<string> = new Set(['edit', 'write', 'delete', 'edit_file', 'write_file'])\n\nfunction isFileEditTool(name: string): boolean {\n return FILE_EDIT_TOOLS.has(name)\n}\n\nexport class AgentObservabilityRegistry extends TypedEventEmitter<ObservabilityEvents> {\n private readonly observations = new Map<string, AgentObservation>()\n private readonly abortFns = new Map<string, () => void>()\n private readonly messagesRefs = new Map<\n string,\n readonly AgentMessage[] | (() => readonly AgentMessage[])\n >()\n private readonly streamingTexts = new Map<string, string>()\n private readonly turns = new Map<string, TurnRecord[]>()\n private readonly evictTimers = new Map<string, ReturnType<typeof setTimeout>>()\n /** 被 pin 的 id 不受驱逐 timer 影响(用于 s 键查看 sub-agent 时阻止消息被清除)。 */\n private readonly pinned = new Set<string>()\n private readonly now: () => number\n private readonly warnMs: number\n private readonly stalledMs: number\n private readonly evictMs: number\n\n constructor(opts: AgentObservabilityOptions = {}) {\n super()\n this.now = opts.now ?? Date.now\n this.warnMs = opts.warnMs ?? 3_000\n this.stalledMs = opts.stalledMs ?? 30_000\n this.evictMs = opts.evictMs ?? 15_000\n }\n\n /** 登记一个待观测 Agent(attachAgentObserver 首先调用)。重复 id 覆盖(同 id 复用即续命)。 */\n register(meta: AgentObservationMeta): void {\n const t = this.now()\n const prevEvict = this.evictTimers.get(meta.id)\n if (prevEvict) {\n clearTimeout(prevEvict)\n this.evictTimers.delete(meta.id)\n }\n if (meta.abort) this.abortFns.set(meta.id, meta.abort)\n if (meta.messages) this.messagesRefs.set(meta.id, meta.messages)\n this.streamingTexts.set(meta.id, '')\n this.turns.set(meta.id, [])\n this.observations.set(meta.id, {\n id: meta.id,\n agentName: meta.agentName,\n title: meta.title,\n prompt: meta.prompt,\n model: meta.model,\n modelDecidedBy: meta.modelDecidedBy,\n origin: meta.origin ?? 'main',\n depth: meta.depth ?? 0,\n status: 'streaming',\n turnIndex: 0,\n toolUseCount: 0,\n filesEdited: 0,\n tokenCount: 0,\n inputTokens: 0,\n lastActivity: 'starting',\n lastEventAt: t,\n abortable: meta.abortable ?? meta.abort !== undefined,\n startedAt: t,\n })\n this.emit('change')\n }\n\n /**\n * 中止一个被观测 agent(面板 `x`/`k` → 这里)。调用其 abort 回调(swarm=agent.abort / detached=\n * 清 pending approval + agent.abort);已结束或无回调返回 false。中止后状态由 abort/status.change 事件收敛。\n */\n abort(id: string): boolean {\n const o = this.observations.get(id)\n const fn = this.abortFns.get(id)\n if (!o || !fn || TERMINAL.has(o.status)) return false\n fn()\n return true\n }\n\n /** 摄入一个引擎事件并归约到观测态。高频 stream delta 只更 lastEventAt、不 notify(防风暴)。 */\n record(id: string, event: AgentEvent): void {\n const o = this.observations.get(id)\n if (!o) return\n o.lastEventAt = this.now()\n let structural = true\n\n switch (event.type) {\n case 'turn.start':\n o.turnIndex = event.turnIndex\n o.status = 'streaming'\n o.lastActivity = 'thinking'\n o.blockedOn = 'model_io'\n o.blockDetail = undefined\n this.streamingTexts.set(id, '')\n this.turns.get(id)?.push({\n index: event.turnIndex,\n startedAt: this.now(),\n toolCount: 0,\n tools: [],\n outputTokens: 0,\n })\n break\n case 'stream.event': {\n const ev = event.event\n if (ev.type === 'text_delta') {\n o.lastActivity = 'responding'\n o.blockedOn = undefined\n this.streamingTexts.set(id, (this.streamingTexts.get(id) ?? '') + ev.delta)\n } else if (ev.type.startsWith('thinking')) {\n o.lastActivity = 'thinking'\n o.blockedOn = undefined\n }\n structural = false\n break\n }\n case 'tool.call.start': {\n o.toolUseCount += 1\n o.status = 'tool_executing'\n o.lastActivity = `using ${event.toolCall.name}`\n o.blockedOn = 'process'\n o.blockDetail = event.toolCall.name\n const cur = this.turns.get(id)\n if (cur?.length) {\n cur[cur.length - 1]!.toolCount += 1\n cur[cur.length - 1]!.tools.push(event.toolCall.name)\n }\n break\n }\n case 'tool.call.end':\n o.status = 'streaming'\n o.lastActivity = 'thinking'\n o.blockedOn = 'model_io'\n o.blockDetail = undefined\n if (isFileEditTool(event.toolCall.name)) o.filesEdited += 1\n break\n case 'turn.end': {\n const out = event.message.usage.outputTokens ?? 0\n o.tokenCount += out\n o.inputTokens = event.message.usage.inputTokens ?? o.inputTokens\n this.streamingTexts.set(id, '')\n const cur = this.turns.get(id)\n const last = cur?.[cur.length - 1]\n if (last && last.endedAt === undefined) {\n last.endedAt = this.now()\n last.outputTokens = out\n }\n break\n }\n case 'stream.retry':\n o.lastActivity = `reconnecting ${event.attempt}/${event.maxRetries}`\n o.blockedOn = 'model_io'\n break\n case 'status.change':\n this.applyTerminal(o, event.to)\n break\n case 'error':\n this.markTerminal(o, 'error')\n break\n case 'abort':\n this.markTerminal(o, 'aborted')\n break\n case 'lifecycle.tool.completed':\n // RFC-144 M1:旁路通知——不驱动状态转换(auto-trigger 归 M3 的 workLoop 控制流),\n // 仅更新 lastActivity 供观测面板展示\"后台任务 X 已完成\"。\n o.lastActivity = `${event.toolName} ${event.isError ? 'failed' : 'completed'} (background)`\n o.lastEventAt = this.now()\n structural = true\n break\n case 'lifecycle.tool.completed.crossBoundary':\n // RFC-144 M4/M6:工具完成时已跨越 prompt 边界——结果不会注入新 prompt 的对话\n // 历史(避免污染无关任务上下文),但用户仍应知道\"后台任务 X 完成了,但结果没有\n // 展示在当前对话里\"(M4 独立评审 MEDIUM 发现的缺口:此前该事件无任何消费者,\n // 用户对跨边界完成的工具毫无感知)。lastActivity 用不同措辞与同 prompt 内完成\n // 区分,明确标注\"结果未注入当前对话\",观测面板/TUI 可据此渲染差异化提示。\n o.lastActivity = `${event.toolName} completed in background (result not shown — belongs to a previous prompt)`\n o.lastEventAt = this.now()\n structural = true\n break\n default:\n structural = false\n break\n }\n\n if (structural) this.emit('change')\n }\n\n /** detached /job 的审批挂起——经此显式标记(裸 Agent 不在事件总线上发 approval)。 */\n setAwaitingApproval(id: string, detail: string): void {\n const o = this.observations.get(id)\n if (!o) return\n o.status = 'awaiting_gate'\n o.lastActivity = 'awaiting approval'\n o.blockedOn = 'approval'\n o.blockDetail = detail\n o.lastEventAt = this.now()\n this.emit('change')\n }\n\n clearAwaitingApproval(id: string): void {\n const o = this.observations.get(id)\n if (!o || o.status !== 'awaiting_gate') return\n o.status = 'streaming'\n o.lastActivity = 'thinking'\n o.blockedOn = 'model_io'\n o.blockDetail = undefined\n o.lastEventAt = this.now()\n this.emit('change')\n }\n\n /**\n * RFC-144 M1 接口占位(M3 接线):lifecycleAsync 工具在后台执行、loop 已 yield 等待。\n * 与 `setAwaitingApproval` 同构(外部显式标记 + 显式清除),供 M3 的 workLoop 控制流\n * 在 post-runTools 检测到 `pendingLifecycleTools` 非空时调用。\n */\n setToolWaiting(id: string, detail: string): void {\n const o = this.observations.get(id)\n if (!o) return\n o.status = 'tool_waiting'\n o.lastActivity = detail\n o.blockedOn = 'process'\n o.blockDetail = detail\n o.lastEventAt = this.now()\n this.emit('change')\n }\n\n clearToolWaiting(id: string): void {\n const o = this.observations.get(id)\n if (!o || o.status !== 'tool_waiting') return\n o.status = 'streaming'\n o.lastActivity = 'thinking'\n o.blockedOn = 'model_io'\n o.blockDetail = undefined\n o.lastEventAt = this.now()\n this.emit('change')\n }\n\n /** 兜底收尾(wiring 站点 finally 调用):若仍活跃则置 done。status.change('completed') 通常已覆盖。 */\n markEnded(id: string, status: AgentObsStatus = 'done'): void {\n const o = this.observations.get(id)\n if (!o || TERMINAL.has(o.status)) return\n this.markTerminal(o, status)\n this.emit('change')\n }\n\n /** 阻止 id 关联的数据被驱逐。取消已排队的 evict timer 并标记为 pinned。\n * - 已终态 agent:timer 被取消,数据保留到 unpin 后重启 timer。\n * - 运行中 agent:无 timer 可取消,仅标记 pinned,未来 markTerminal 的 scheduleEvict 会跳过。\n * 重复 pin 幂等。 */\n pin(id: string): void {\n const prev = this.evictTimers.get(id)\n if (prev) {\n clearTimeout(prev)\n this.evictTimers.delete(id)\n }\n this.pinned.add(id)\n }\n\n /** 取消 id 的 pin 保护。若 agent 已终态(endedAt 存在),重启驱逐 timer(默认 evictMs)。\n * 若仍在运行中(未结束),不操作——终态时 markTerminal 会自行 scheduleEvict。\n * 重复 unpin 幂等。 */\n unpin(id: string): void {\n this.pinned.delete(id)\n const obs = this.observations.get(id)\n if (obs && obs.endedAt !== undefined) {\n this.scheduleEvict(id)\n }\n }\n\n /**\n * 从表中立即移除(面板手动删除一项,不等自动驱逐 timer)。终态/运行中皆可调——运行中直接\n * 静默移除观测记录(不中止真实 agent,中止走 abort();本方法只影响 UI 可见性)。\n * 清 evictTimer/abortFn/messagesRef/streamingText/turns/pinned,幂等(不存在的 id 安全跳过)。\n */\n remove(id: string): void {\n const timer = this.evictTimers.get(id)\n if (timer) {\n clearTimeout(timer)\n this.evictTimers.delete(id)\n }\n this.observations.delete(id)\n this.abortFns.delete(id)\n this.messagesRefs.delete(id)\n this.streamingTexts.delete(id)\n this.turns.delete(id)\n this.pinned.delete(id)\n this.emit('change')\n }\n\n get(id: string): AgentObservationSnapshot | undefined {\n const o = this.observations.get(id)\n return o ? this.toSnapshot(o) : undefined\n }\n\n /** P1 视图A:取某 agent 转录的**快照副本**(防消费方读到后被 agent 继续 mutate 的活引用)。\n * ref 可为数组或取值函数(AgentSession 子代理传 getter,每次取最新)。 */\n getMessages(id: string): AgentMessage[] | undefined {\n const ref = this.messagesRefs.get(id)\n if (!ref) return undefined\n const arr = typeof ref === 'function' ? ref() : ref\n return [...arr]\n }\n\n /** 冻结某 agent 转录终态快照。task-runner 在 ephemeral removeSession 前调,\n * 使切入对话/Transcript 在子会话被 dispose 后仍可读最终内容(驱逐前 evictMs 窗口内)。 */\n setMessages(id: string, msgs: readonly AgentMessage[]): void {\n if (this.messagesRefs.has(id)) this.messagesRefs.set(id, [...msgs])\n }\n\n /** P1 视图A:当前 turn 尚未落入 messages 的 in-flight 流式文本(无则空串)。 */\n getStreamingText(id: string): string {\n return this.streamingTexts.get(id) ?? ''\n }\n\n /** P2 视图B:单轮记录(turn timeline)副本;无则空数组。 */\n getTurns(id: string): TurnRecord[] {\n const t = this.turns.get(id)\n return t ? t.map((r) => ({ ...r, tools: [...r.tools] })) : []\n }\n\n /** 全部观测快照(含派生 idleMs/stall)。 */\n list(): AgentObservationSnapshot[] {\n return [...this.observations.values()].map((o) => this.toSnapshot(o))\n }\n\n /** 仅活跃(未结束)的观测。 */\n listActive(): AgentObservationSnapshot[] {\n return this.list().filter((s) => !TERMINAL.has(s.status))\n }\n\n subscribe(listener: () => void): () => void {\n return this.on('change', listener)\n }\n\n /** 测试用:清空。 */\n clear(): void {\n for (const t of this.evictTimers.values()) clearTimeout(t)\n this.evictTimers.clear()\n this.observations.clear()\n this.abortFns.clear()\n this.messagesRefs.clear()\n this.streamingTexts.clear()\n this.turns.clear()\n this.pinned.clear()\n }\n\n private applyTerminal(o: AgentObservation, to: AgentStatus): void {\n if (to === 'completed') this.markTerminal(o, 'done')\n else if (to === 'error') this.markTerminal(o, 'error')\n else if (to === 'aborted') this.markTerminal(o, 'aborted')\n }\n\n private markTerminal(o: AgentObservation, status: AgentObsStatus): void {\n o.status = status\n o.lastActivity = status\n o.blockedOn = undefined\n o.blockDetail = undefined\n o.endedAt = this.now()\n this.scheduleEvict(o.id)\n }\n\n private scheduleEvict(id: string): void {\n if (this.pinned.has(id)) return\n const prev = this.evictTimers.get(id)\n if (prev) clearTimeout(prev)\n const timer = setTimeout(() => {\n this.observations.delete(id)\n this.evictTimers.delete(id)\n this.abortFns.delete(id)\n this.messagesRefs.delete(id)\n this.streamingTexts.delete(id)\n this.turns.delete(id)\n this.emit('change')\n }, this.evictMs)\n timer.unref?.()\n this.evictTimers.set(id, timer)\n }\n\n private toSnapshot(o: AgentObservation): AgentObservationSnapshot {\n const ended = o.endedAt !== undefined\n const idleMs = ended ? 0 : Math.max(0, this.now() - o.lastEventAt)\n const stall: AgentObservationSnapshot['stall'] = ended\n ? 'none'\n : idleMs > this.stalledMs\n ? 'stalled'\n : idleMs > this.warnMs\n ? 'warn'\n : 'none'\n return { ...o, idleMs, stall }\n }\n}\n\n/** 全局单例(照 globalProcessRegistry / globalAgentJobRegistry)。 */\nexport const globalAgentObservability = new AgentObservabilityRegistry()\n\n/**\n * ① AgentObserver:把一个裸 Agent 的事件转发进观测注册表(路 B——不升格 AgentSession、不动执行语义、\n * 保留转录隔离;observer 只读)。在 swarm-runtime / detached-run 的 `new Agent` 后调用;wiring 站点\n * 应在 `await agent.run()` 的 finally 里调返回的 detach + `registry.markEnded(id)` 兜底收尾。\n *\n * @returns detach 函数:解订阅所有事件监听(不删观测,结束态由 status.change/abort/error + markEnded 处理)。\n */\nexport function attachAgentObserver(\n agent: Agent,\n meta: AgentObservationMeta,\n registry: AgentObservabilityRegistry = globalAgentObservability,\n): () => void {\n registry.register(meta)\n const id = meta.id\n const unsubs: Array<() => void> = [\n agent.on('turn.start', (p) => registry.record(id, { type: 'turn.start', ...p })),\n agent.on('turn.end', (p) => registry.record(id, { type: 'turn.end', ...p })),\n agent.on('stream.event', (p) => registry.record(id, { type: 'stream.event', ...p })),\n agent.on('tool.call.start', (p) => registry.record(id, { type: 'tool.call.start', ...p })),\n agent.on('tool.call.end', (p) => registry.record(id, { type: 'tool.call.end', ...p })),\n agent.on('status.change', (p) => registry.record(id, { type: 'status.change', ...p })),\n agent.on('stream.retry', (p) => registry.record(id, { type: 'stream.retry', ...p })),\n agent.on('error', (p) => registry.record(id, { type: 'error', ...p })),\n agent.on('abort', () => registry.record(id, { type: 'abort' })),\n // RFC-144 M6(补齐缺口):此前 `record()` 的 reducer 已经实现了这两个 case 分支\n // (M1/M4 阶段),但 `attachAgentObserver` 从未真正订阅对应的 Agent 事件——\n // reducer 分支是死代码,观测面板永远不会展示\"后台 lifecycleAsync 工具已完成\"。\n agent.on('lifecycle.tool.completed', (p) =>\n registry.record(id, { type: 'lifecycle.tool.completed', ...p }),\n ),\n agent.on('lifecycle.tool.completed.crossBoundary', (p) =>\n registry.record(id, { type: 'lifecycle.tool.completed.crossBoundary', ...p }),\n ),\n ]\n return () => {\n for (const u of unsubs) u()\n }\n}\n","import { defineHook } from '@x-otto/hooks'\n\nimport { SYSTEM_PROMPT_SECTIONS } from './section-order'\nimport type { HookSpec } from '@x-otto/hooks'\n\n/**\n * memory-delta(#3 volatile memory delta)\n *\n * session-stable 的 memory-index 把 memory 注入按 sessionId 冻结在 turn-1 快照(cache 锚点),\n * 代价是会话中途 `memory_record`/编辑 AGENTS.md 的更新要到下一会话才生效。\n *\n * 本钩子补这个缺口:每轮把「相对 turn-1 基线发生变化的 memory 文件」以 **volatile 尾段**\n * (output.systemTail,落在 prompt cache 断点之后)即时注入——既即时生效又不击穿会话稳定的\n * memory 前缀。基线在首轮(memory 加载后)快照,session.deleted 驱逐。\n *\n * 注意:priority 必须 > memory-index(35),确保首轮捕获基线时 memory 已加载。\n *\n * **预算门(system prompt 体量治理 P0 的旁路缺口修复)**:`buildMemoryInjection`\n * (`@x-otto/memory`)给会话首轮的稳定 memory 前缀加了总预算门,但本文件独立格式化\n * volatile delta、不经过那道门——会话内多次 `memory_record` 大内容或反复编辑大文件,\n * delta 段可无界累积进 `systemTail`。`runtime` 不依赖 `@x-otto/memory`(避免引入跨包\n * 耦合/潜在循环依赖),故预算逻辑在本文件内自持一份最小实现,不与 `@x-otto/memory`\n * 共享代码——两处判断口径一致(同款 CJK 加权估算 + 字符边界截断),但物理独立。\n */\nexport interface MemoryDeltaPort {\n /** 当前 memory 源映射(path → content)。MemoryManager.getMemories() 即此形态。 */\n getMemories(): ReadonlyMap<string, string>\n}\n\n/** delta 段默认总预算(tokens)。同 `@x-otto/memory` 的 `DEFAULT_MEMORY_INJECTION_BUDGET_TOKENS`\n * 同一量级考量,但 delta 是逐轮追加的volatile 段,不该比 stable 前缀本身更大——取半档。 */\nexport const DEFAULT_MEMORY_DELTA_BUDGET_TOKENS = 4_000\n\nfunction estimatedTokens(text: string): number {\n let cjk = 0\n for (const ch of text) {\n if (/[\\u3000-\\u9fff\\uff00-\\uffef]/.test(ch)) cjk++\n }\n return Math.ceil(cjk + (text.length - cjk) / 4)\n}\n\nfunction truncateToTokenBudget(text: string, remainingTokens: number): string {\n if (remainingTokens <= 0) return ''\n const approxChars = remainingTokens * 4\n const chars = Array.from(text)\n if (chars.length <= approxChars) return text\n return chars.slice(0, approxChars).join('')\n}\n\nexport type MemoryDelta =\n | { kind: 'upsert'; path: string; content: string }\n | { kind: 'remove'; path: string }\n\nexport function formatMemoryDelta(\n changed: ReadonlyArray<readonly [string, string]> | ReadonlyArray<MemoryDelta>,\n budgetTokens: number = DEFAULT_MEMORY_DELTA_BUDGET_TOKENS,\n): string {\n const parts = ['<agent_memory_updates note=\"本会话中途更新——取代上方缓存中的对应文件\">']\n let usedTokens = 0\n let skippedCount = 0\n\n for (const entry of changed) {\n const delta: MemoryDelta = Array.isArray(entry)\n ? { kind: 'upsert', path: entry[0], content: entry[1] }\n : entry as MemoryDelta\n const remaining = budgetTokens - usedTokens\n if (remaining <= 0) {\n skippedCount++\n continue\n }\n\n if (delta.kind === 'remove') {\n const notice = `[removed during this session — disregard the cached version of ${delta.path} above]`\n const noticeTokens = estimatedTokens(notice)\n if (noticeTokens > remaining) {\n skippedCount++\n continue\n }\n parts.push(`# ${delta.path}`)\n parts.push(notice)\n parts.push('')\n usedTokens += noticeTokens\n continue\n }\n\n const { path, content } = delta\n const trimmed = content.trim()\n if (!trimmed) continue\n\n // `remaining` is computed before every entry so removal notices participate in\n // the same bounded volatile-tail budget as content updates.\n if (remaining <= 0) {\n skippedCount++\n continue\n }\n\n const contentTokens = estimatedTokens(trimmed)\n if (contentTokens <= remaining) {\n parts.push(`# ${path}`)\n parts.push(trimmed)\n parts.push('')\n usedTokens += contentTokens\n } else {\n const truncated = truncateToTokenBudget(trimmed, remaining)\n parts.push(`# ${path}`)\n parts.push(truncated)\n parts.push(`\\n> ⚠ [truncated — content exceeded the memory delta budget (${budgetTokens} tokens); remainder omitted]`)\n parts.push('')\n usedTokens = budgetTokens\n }\n }\n\n if (skippedCount > 0) {\n parts.push(`> ⚠ [${skippedCount} additional changed memory source(s) omitted — delta budget (${budgetTokens} tokens) reached]`)\n parts.push('')\n }\n\n parts.push('</agent_memory_updates>')\n return parts.join('\\n')\n}\n\nexport function createMemoryDeltaHooks(\n memory: MemoryDeltaPort,\n priority = SYSTEM_PROMPT_SECTIONS.memoryDelta.priority,\n): HookSpec[] {\n // Baseline is the cached stable prefix. Tombstones persist until session deletion:\n // later turns still receive the stable prefix, so a one-turn removal notice is not enough.\n const baselines = new Map<string, Map<string, string>>()\n const tombstones = new Map<string, Set<string>>()\n\n const inject = defineHook({\n name: 'memory-delta-injection',\n timing: 'system.prompt.transform',\n priority,\n handle: (input, output) => {\n const current = memory.getMemories()\n\n const baseline = baselines.get(input.sessionId)\n if (!baseline) {\n baselines.set(input.sessionId, new Map(current))\n return\n }\n\n const removed = tombstones.get(input.sessionId) ?? new Set<string>()\n for (const path of baseline.keys()) {\n if (!current.has(path)) removed.add(path)\n }\n for (const path of current.keys()) {\n // Re-creation supersedes the old removal notice for this stable-prefix path.\n removed.delete(path)\n }\n tombstones.set(input.sessionId, removed)\n\n const changed: MemoryDelta[] = []\n for (const path of removed) {\n changed.push({ kind: 'remove', path })\n }\n for (const [path, content] of current) {\n if (baseline.get(path) !== content) {\n changed.push({ kind: 'upsert', path, content })\n }\n }\n if (changed.length === 0) return\n\n output.systemTail ??= []\n output.systemTail.push(formatMemoryDelta(changed))\n },\n })\n\n const cleanup = defineHook({\n name: 'memory-delta-cleanup',\n timing: 'session.deleted',\n priority: 90,\n handle: (input) => {\n const sessionId = (input as { sessionId: string }).sessionId\n baselines.delete(sessionId)\n tombstones.delete(sessionId)\n },\n })\n\n return [inject, cleanup]\n}\n","import { defineHook } from '@x-otto/hooks'\n\nimport { SYSTEM_PROMPT_SECTIONS } from './section-order'\nimport type { HookSpec } from '@x-otto/hooks'\n\n/**\n * todo-reminder(RFC-042)\n *\n * 病灶:`✓ done` 是回合生命周期标记,与 write_todos 的 todo 完成度零耦合——模型可在 todo 仍有\n * pending/in_progress 时 end_turn,UI 却照显 done(见 RFC-042 根因 1/3)。\n *\n * 本钩子实现 todo 护栏:每次 buildContext 时,若当前会话 todoList 仍有未完成项,就把最新\n * todo 状态作为 **volatile 尾段**(output.systemTail,落在 prompt cache 断点之后)注入一个\n * `<system-reminder>`,让模型在下一次推理时\"看见\"未竟工作从而继续/纠偏。\n *\n * 关键约束(RFC-042 重要事项规则 1/3):\n * - 只进 volatile systemTail,绝不进 stable 前缀——否则逐轮击穿 prompt cache([[RFC-018]])。\n * - 数据源 = session.metadata().config?.todoList(持久化真源,经稳定 Session 接口读取),\n * 不耦合 CLI projState;故无 per-session 基线、无状态、无 cleanup(与 memory-delta 不同)。\n * - buildContext 每用户 prompt 只跑一次,故 reminder 在\"即将停\"的当轮无效——它靠续轮(M74-04)\n * 触发的下一次 buildContext 生效,二者是耦合闭环(RFC-042 D1)。\n */\nexport interface TodoReminderPort {\n /**\n * 取指定会话的 todo 快照(read-only)。返回 undefined / 空数组表示无 todo。\n * 形态 = session.metadata().config?.todoList。\n */\n getTodoList(\n sessionId: string,\n ): ReadonlyArray<{ id?: string; title?: string; status?: string }> | undefined\n}\n\n/** todo 终态集合(与 reduce.ts countIncompleteTasks / interactive.ts 清理逻辑一致)。 */\nconst TERMINAL_STATUSES = new Set(['done', 'skipped', 'failed'])\n\nexport function isIncompleteTodo(status: string | undefined): boolean {\n return !TERMINAL_STATUSES.has(status ?? 'pending')\n}\n\nexport function formatTodoReminder(\n todos: ReadonlyArray<{ title?: string; status?: string }>,\n): string {\n const incomplete = todos.filter((t) => isIncompleteTodo(t.status)).length\n const lines = [\n `<system-reminder>`,\n `Your todo list still has ${incomplete} unfinished item(s). Do not treat the task as complete — and do not end your turn — while any item is pending or in_progress. Either finish the remaining work and mark each item done, or explicitly mark items skipped/failed with a reason. Latest todo state:`,\n ]\n for (const t of todos) {\n lines.push(`- [${t.status ?? 'pending'}] ${t.title ?? ''}`)\n }\n lines.push(\n `Continue with the unfinished items now. (This reminder is system-generated from your todo list; do not repeat it back to the user.)`,\n )\n lines.push(`</system-reminder>`)\n return lines.join('\\n')\n}\n\nexport function createTodoReminderHooks(\n port: TodoReminderPort,\n priority = SYSTEM_PROMPT_SECTIONS.todoReminder.priority,\n): HookSpec[] {\n const inject = defineHook({\n name: 'todo-reminder-injection',\n timing: 'system.prompt.transform',\n priority,\n handle: (input, output) => {\n const todos = port.getTodoList(input.sessionId)\n if (!todos || todos.length === 0) return\n if (!todos.some((t) => isIncompleteTodo(t.status))) return\n\n output.systemTail ??= []\n output.systemTail.push(formatTodoReminder(todos))\n },\n })\n\n return [inject]\n}\n","import { defineHook } from '@x-otto/hooks'\n\nimport { SYSTEM_PROMPT_SECTIONS } from './section-order'\nimport type { HookSpec, ToolExecuteAfterInput, SessionEventInput } from '@x-otto/hooks'\n\n/**\n * doc-sync-reminder(RFC-100)\n *\n * 病灶:write_todos(会话级 UI 状态)与外部持久化文档(如 RFC/TODO/milestone markdown)是两套\n * 完全独立、不会自动互相同步的记账系统。模型在多阶段任务中容易把 write_todos 当\"第一阶段的一次性\n * 草稿本\"用完即弃,转而只维护文档,导致 UI 常驻 todo 清单停留在早期快照,与仓库真实进度脱节\n * (lesson_50 记录的真实复现案例)。\n *\n * 本钩子抄 todo-reminder.ts 同一模式(system-reminder 注入护栏),但检测的是\"外部文档同步\"而非\n * \"todo 内部完成度\"——两者互补,不重叠:\n * - todo-reminder 检测「todoList 内部有 pending/in_progress 却想结束回合」\n * - doc-sync-reminder 检测「本回合写过被判定为需同步的文档,但没有调用 write_todos」\n *\n * 判定\"什么算需要同步的文档变更\"完全下放给调用方注入的 DocSyncPort(不硬编码任何本仓库专属的\n * 文件名/内容模式到 runtime 通用层——对不使用这套约定的项目零污染,不注册这个 hook 即可)。\n *\n * 状态机:\n * tool.execute.after → 累积\"本轮是否触碰过需同步文档\" + \"本轮是否调用过 write_todos\"\n * chat.message.before → 新用户消息到达 = 新回合开始,重置累积状态\n * system.prompt.transform → 若\"触碰过需同步文档\"且\"未调用 write_todos\",注入提醒\n * session.deleted → 驱逐会话状态(同 memory-delta.ts 模式)\n */\nexport interface DocSyncPort {\n /**\n * 判定一次工具调用是否构成\"需要同步 write_todos 的文档变更\"。\n * 典型实现:检查 toolName 是否为 edit/write、args.path 是否匹配约定的文档命名模式(如\n * `*-milestone.md`)、写入的新内容是否包含状态翻转标记(如 `Status: Done`)。\n * 返回 true 即计入\"本轮触碰过需同步文档\"。\n */\n isDocSyncTrigger(input: ToolExecuteAfterInput): boolean\n}\n\ninterface TurnState {\n touchedDoc: boolean\n toolTouchedDoc: string | undefined\n calledWriteTodos: boolean\n}\n\nfunction freshState(): TurnState {\n return { touchedDoc: false, toolTouchedDoc: undefined, calledWriteTodos: false }\n}\n\nexport function formatDocSyncReminder(toolName: string): string {\n return [\n `<system-reminder>`,\n `You just modified a tracked progress document (via ${toolName}) but have not called write_todos ` +\n `in this turn. write_todos (the UI-pinned todo list) and progress documents are two independent ` +\n `tracking systems that do not auto-sync. If this document edit reflects real progress (e.g. a ` +\n `milestone Status flip to Done), call write_todos now to keep the pinned list consistent with the ` +\n `document. (This reminder is system-generated; do not repeat it back to the user.)`,\n `</system-reminder>`,\n ].join('\\n')\n}\n\nexport function createDocSyncReminderHooks(\n port: DocSyncPort,\n priority = SYSTEM_PROMPT_SECTIONS.docSyncReminder.priority,\n): HookSpec[] {\n const stateBySession = new Map<string, TurnState>()\n\n const track = defineHook({\n name: 'doc-sync-tool-tracker',\n timing: 'tool.execute.after',\n priority: 90,\n handle: (input) => {\n const state = stateBySession.get(input.sessionId) ?? freshState()\n\n if (input.toolName === 'write_todos') {\n state.calledWriteTodos = true\n } else if (!state.touchedDoc && port.isDocSyncTrigger(input)) {\n state.touchedDoc = true\n state.toolTouchedDoc = input.toolName\n }\n\n stateBySession.set(input.sessionId, state)\n },\n })\n\n const reset = defineHook({\n name: 'doc-sync-turn-reset',\n timing: 'chat.message.before',\n priority: 10,\n handle: (input) => {\n // RFC-094 todo 闸门的续跑轮(TODO_GATE_CONTINUE_PROMPT)也走 runPromptRound →\n // 无条件触发 chat.message.before(pipeline.ts,internal 消息不豁免)。若在此重置,\n // \"改了文档没同步 todo → 被闸门拉回续跑\" 这个**最该被提醒的场景**的状态会在提醒\n // 有机会注入(续跑轮的 system.prompt.transform)之前被清零——终局 review 用可控\n // 实验证实的真实交互 bug。internal 续跑是同一逻辑回合的延续,不是新用户回合,跳过重置。\n const message = input.message as { internal?: boolean }\n if (message.internal === true) return\n stateBySession.set(input.sessionId, freshState())\n },\n })\n\n const inject = defineHook({\n name: 'doc-sync-reminder-injection',\n timing: 'system.prompt.transform',\n priority,\n handle: (input, output) => {\n const state = stateBySession.get(input.sessionId)\n if (!state?.touchedDoc || state.calledWriteTodos) return\n\n output.systemTail ??= []\n output.systemTail.push(formatDocSyncReminder(state.toolTouchedDoc ?? 'edit/write'))\n },\n })\n\n const cleanup = defineHook({\n name: 'doc-sync-cleanup',\n timing: 'session.deleted',\n priority: 90,\n handle: (input: SessionEventInput) => {\n stateBySession.delete(input.sessionId)\n },\n })\n\n return [track, reset, inject, cleanup]\n}\n","/**\n * residency-allocator.ts — RFC-323:进程级驻留总预算与会话间分配。\n *\n * ⚠️ **生产路径已由 RFC-325 的 `global-residency-engine` 接管**(2026-08-10)。\n * 本模块**当前无生产消费方**,但**刻意保留**,理由与 RFC-325 M1(已删)不同:\n * 它是引擎向后兼容性的**独立测试预言(oracle)**——\n * `rfc325-m2-global-residency-engine.test.ts:95` 断言 `engine(pressure=0) === allocator(N)`,\n * 若删掉它,该断言只能退化为\"引擎和自己比\",就再也证明不了\n * \"新引擎在无压力时与旧模型逐位一致\"这条回归保证。\n *\n * 即:保留成本 = 58 行不参与运行的纯函数;删除成本 = 失去一条真实的回归锚。\n * 若将来负反馈模型被永久接受、不再需要与旧模型对照,可连同该断言一并删除。\n *\n * 解决 RFC-321 C2(第八轮设计 review 产出):per-session 固定预算(64MB)在单会话\n * 场景过度保守(堆里 2.5GB 空着只用了 5%),多会话场景又无总量保证(N 个会话各 64MB\n * 无上限)。本分配器以\"全局池 + 公平份额\"模型替换固定值:\n *\n * effectiveBudget = clamp(TOTAL / N, MIN, MAX)\n *\n * N=1 时可用满 MAX(256MB,现状 4 倍);N=10 时各 102MB(仍宽于现状);N=50 时\n * 各 20MB(自动收紧),总量始终 ≤ TOTAL 有界。\n *\n * 设计约束(RFC-323 R2/R4):\n * - **确定性**:给定 N 与压力档,有效预算唯一确定;不引入采样值/时间/随机性(R4)。\n * - **heapUsed 不作为分配依据**(R2,RFC-321 R11 维持):只用\"会话数\"这个确定量。\n * - **纯函数**:无状态、无副作用、可单测;分配器本身不感知会话树,由调用方传 N。\n */\nimport { resolveResidencyBudget } from '@x-otto/session-contract'\n\nimport type { ResidencyBudgetConfig } from '@x-otto/session-contract'\n\n/**\n * 计算单会话的有效字节预算(分配层,不含压力档缩放)。\n *\n * @param activeSessionCount 当前有驻留的活跃会话数(transient 排除,见 RFC-323 D4)\n * @param config RFC-323 M4:运行时预算配置(缺省回退 session-contract 常量/env)。\n * settings.json 覆盖经此参数注入——不能靠\"写 env\",因常量在模块加载时已固化(D6b)。\n * @returns clamp(TOTAL / N, MIN, MAX),N <= 0 时回退 MAX(防御)\n */\nexport function allocateResidencyBudget(\n activeSessionCount: number,\n config?: ResidencyBudgetConfig,\n): number {\n const { totalBytes, minPerSessionBytes, maxPerSessionBytes } = resolveResidencyBudget(config)\n if (activeSessionCount <= 0) return maxPerSessionBytes\n const raw = Math.floor(totalBytes / activeSessionCount)\n return Math.max(minPerSessionBytes, Math.min(raw, maxPerSessionBytes))\n}\n\n/**\n * RFC-323 D3:两层叠加——分配层 × 压力档缩放。\n *\n * 分配层解决\"并发数不确定\"(结构性、确定性);\n * 压力层解决\"真实内存压力超预期\"(反应式、RFC-178 触发器 C 保留)。\n *\n * 两者相乘:`allocateResidencyBudget(N) ÷ 压力档除数(1/2/4)`。\n * 下界 clamp(MIN ÷ 除数, 1) 防止极端收紧塌到 0。\n */\nexport function effectiveResidencyBudget(\n activeSessionCount: number,\n pressureDivisor: number,\n config?: ResidencyBudgetConfig,\n): number {\n const { minPerSessionBytes } = resolveResidencyBudget(config)\n const allocated = allocateResidencyBudget(activeSessionCount, config)\n const floor = Math.max(1, Math.floor(minPerSessionBytes / pressureDivisor))\n return Math.max(floor, Math.floor(allocated / pressureDivisor))\n}\n"],"mappings":"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"}