CLAUDE.md · diff

git:20261015.468e8b9 to git:20261016.dbd0615

4 added, 2 removed. Audit A to A.

# CLAUDE.md — 灵枢 LingShu 项目 Claude Code 引导
> 给 Claude Code 的项目级上下文。**每个新 Claude 会话 turn 1 自动加载。**
>
> **配套文档**(按需加载,不重复):
> - 设计文档:[`dsh_agent_design.md`](./dsh_agent_design.md) v1.5.60 / 7450 行 / 项目真理(v1.5.34 §7.1 新增子节 —— 明确「AgentFactory 是 Spring 单例 Bean(无状态,持 7 Router),Agent 是 factory 的产品(prototype-like,带 session/config/engine 状态,每次 create 一份,Spring 不持有引用)」 — 5 维度对比论证为什么 Agent 不能是 Spring 单例(多轮 session 隔离 / 子 Agent 共享 / A2A 多 RemoteAgent / 测试 Mock / Spring Bean 语义);Agent 生命周期时序图(Spring startup → T0 loadYaml → T1 create 7 项校验 + flowRouter.resolve → T2 run ReAct → T3 流关闭 → T4 GC → T5 session 持久化);扩展点矩阵(7 Router × 9 Slot —— 用户扩展 Provider 不扩展 Router / AgentFactory / Agent 三件套);反模式 `AgentHolder` 5 个失败场景(session 污染 / config 漂移 / 子 Agent 反模式 / A2A 不可行 / 违反 §4.1 不变项);§7.1.6 一句话总结 + 精读章节更新;**纯文档改动**,代码逻辑零改动;7039 → ~7250 行(+约 210);v1.5.33 §5.6.3.0 新增四个核心类型完整定义(`AgentCard` Lombok @Data + 4 nested type / `AgentRef` + `AgentRefBuilder` 装配器 priority 去重排序 / `RemoteAgentSchemaBuilder` @Component 启动期扫 `AgentCard.skills[]` 动态生成 `ToolSpec` list / `AgentCardCache` @Component TTL 缓存 + 负缓存短 TTL=ttl/4 + FIFO evict maxEntries=1000 + `Stats` inner class 命中率指标 + `invalidate()` 显式失效为 §14.8 hot-reload 预留钩子) — 6447 → 7039 行(+592);(`Skill` 接口定义 + `SkillTool.fromMarkdown` 静态工厂 + `@Component implements Skill` 硬编码对照示例) — **问题** v1.5.31 §6.4 把 Skill 多源自动发现 + SkillLoader + SkillTool 链路讲清楚,但"Skill 是什么"含糊 —— 全文反复写 "Skill extends Tool" 但读者翻 §4.6 也找不到 `Skill` 接口定义,只能从 §6.4 `SkillTool implements Skill` 反推;且 `SkillTool.fromMarkdown(name, content)` 静态工厂在 ClasspathSkillSource (L3587) + DirectorySkillSource (L3636) 各被引用一次但无人定义;且只展示「`SKILL.md` → `SkillTool`」文件加载路径,缺「硬编码 `@Component`」对照路径 —— 实施者写内置命令时不知道有第二条路;**根因** §6.4 v1.5.0 写时只把 Skill 当成 Tool 的子集用,缺接口契约 + 工厂方法 + 两种实现路径对照三件套;v1.5.18 后 Tool 体系大改(§6.5 三种 Scheme 来源 + §4.6 ToolExecutor 接口定义补全),Skill 这一支没同步补全契约层;**补丁** (1) **§6.4 L3476-3500 新增 `Skill` 接口** —— `extends Tool`,现阶段零额外方法,Javadoc 明确「Skill 是 Tool 的约定性 marker」+ 与普通 Tool 的 4 点差异(注册进 2 张表 / schema 暴露给模型 / CLI `/xxx` 拦截 / §6.4 全节围绕)+ 3 条未来扩展空间(用户别名 `/c` → `commit` / 权限标记 只能用户触发 / 危险等级 联动 §4.7 审批门);(2) **§6.4 L3783-3790 `SkillTool` 加 `fromMarkdown(name, markdownContent)` 静态工厂** —— SKILL.md 第一行 `# title` 去 leading `#` 提取为 `description`,剩余正文作为 `content`,inputSchema 固定 `{ "input": string }`(与 /xxx <arg> 调用习惯对齐);Javadoc 标注被 ClasspathSkillSource (L3587) + DirectorySkillSource (L3636) 调用;(3) **§6.4 L3810-3852 新增 `@Component CommitSkill implements Skill` 硬编码对照示例** —— 不依赖 SKILL.md 文件,适合"硬编码"内置命令(本例 `/commit` 按 Conventional Commits 风格生成 commit message);附 SkillTool vs @Component 对照表 6 行(来源 / 热加载 / 适合 / 配置 / 推荐)+ 选型决策 3 条(改 skill 行为 → SkillTool / 改 skill 实现逻辑 调外部 API → @Component / 同名 Skill 同时存在走 `putIfAbsent` 先注册者优先);(4) **关键不变项** —— `Tool` 接口 / `SkillLoader` / `CompositeSkillLoader` 行为 / `ToolRegistry` 注册路径 / `ToolExecutor` 5 步流水线 / §4.7 PermissionPolicy / AuditLogger / Cost 域 全部不变;**纯文档改动**,代码逻辑零改动;6395 → 6447 行(+52);**修复者**:Claude Code(根据用户 2026-09-17 会话反馈,用户问「§6.4 Skill 这一节是不是补充一个 Skill 的代码示例会更好的理解啊」,确认 §6.4 缺 `Skill` 接口定义 + `fromMarkdown` 工厂 + 硬编码 `@Component Skill` 对照示例,要求补)v1.5.31 §5.6.3 L2429-2444 `RemoteAgentToolProvider` 误用 `@AutoService(ToolProvider.class)` 修复 —— 降级为普通 `Tool` 模式 — **问题** v1.5.30 §5.6.3 草图用 `@AutoService(ToolProvider.class)` 标注 `RemoteAgentToolProvider`(L2429-2444 注释写「复用 §6.5 Tool 注册路径」),但 §5.7 v1.5.21 明确选择 Spring Boot SPI 而**不**选 Java SPI / OSGi / ClassLoader 隔离,`@AutoService` 是 Google auto-service 库的注解生成 Java SPI 的 `META-INF/services/` 文件 —— 与 §5.7 SPI 决策直接冲突;且 §5.5 / §6.5 全部 Tool / MemorySource / SkillSource 都用 `@Component` 或 `@AutoConfiguration` + `@Bean` 模式注册,**没有任何 ToolProvider 抽象**;`ToolProvider` 接口本身在文档中也未定义(grep 整篇只有 2 处引用,都在 L2430-2431)——「引用了不存在的契约」三重 bug;**根因** v1.5.4 §5.6 引入 A2A 时,`RemoteAgentTool` 曾短暂被设计为 `ToolProvider` 抽象(Slot 10 候选),后续 v1.5.30 §5.6.3 落地时降级为普通 Tool 模式但代码块没同步改 ——留下 `@AutoService` + 不存在的 `ToolProvider` 接口双重历史 bug;**补丁** (1) **§5.6.3 L2429-2444 改写**:移除 `RemoteAgentToolProvider` + `@AutoService` 模式,改写为标准 `@Component public class RemoteAgentTool implements Tool` + `RemoteAgentToolAutoConfiguration`(`@AutoConfiguration` + `@Bean public Tool remoteAgentTool()`,与 §5.5 plugin 非 Slot 类型 Bean 样板对齐);`RemoteAgentTool.execute(call, ctx)` 内部按 `call_<agentName>` 名字 parse 出 agentName + 转发给 `A2aTransport.submit()`;inputSchema 启动期扫 `AgentCard.skills[]` 动态生成(JSON Schema via `RemoteAgentSchemaBuilder`);(2) **§5.6.1 L2346 对照表 `Tool 发现机制` 单元格修正**:原版写「`@AutoService` 启动期静态注册」(Java SPI 措辞)→ 改「Spring `@Component` / `@AutoConfiguration` + `@Bean` 启动期静态注册」(与 §6.5 (1) ReadTool / §6.5 (2) McpToolAdapter / §5.5 plugin 非 Slot 类型 Bean 样板对齐);(3) **§0 L1 + §13 + CLAUDE.md / SKILL / SOP / prompts 版本号同步**;(4) **关键不变项** —— `A2aTransport` 5 方法契约不变 / Slot 9 SPI 不变(仍 9 个 Slot)/ A2aTransportRouter 行为不变 / RemoteAgentTool 内部不变(只看 A2aTransport 接口)/ ToolExecutor 5 步流水线不变 / §4.7 PermissionPolicy / AuditLogger / Cost 域 兼容;**纯文档改动**,代码逻辑零改动;6377 → 6395 行(+18);**修复者**:Claude Code(根据用户 2026-09-17 会话反馈,用户问「`@AutoService(ToolProvider.class)` 是自定义注解还是哪个库的注解啊」+「给出的示例里面为啥会有 `@AutoService(ToolProvider.class)`」,确认 §5.6.3 误用 Java SPI 注解 + `ToolProvider` 接口未定义,与 §5.7 决策冲突,要求降级为普通 Tool 模式)v1.5.30 §5.6.3.1 新增 `HttpJsonRpcA2aTransport` concrete class + `HttpJsonRpcA2aTransportProvider` concrete Provider 完整示例 + §5.6.3.2 备选 `GrpcA2aTransport` / `InProcessA2aTransport` 「3 件套模式」扩展指南 — §5.6.3 L2395-2451 草图用匿名 inner class 形态写 `HttpJsonRpcA2aTransportAutoConfiguration` 的 `@Bean` 方法,但 §5.6.4 L2465 SPI 总表 Slot 9 行的「默认 Provider」字段已经写 `HttpJsonRpcA2aTransportProvider` —— 命名不一致;且 `HttpJsonRpcA2aTransport` 自身也只列名未给 class 定义,用户合理怀疑它是不是 `A2aTransport` 的子接口;**根因** v1.5.4 §5.6 引入 A2A 时,3 个 A2aTransport 变体是平级概念,设计意图是「`A2aTransport` SPI + 3 concrete class 平级实现」,但 §5.6.3 草图偷懒用匿名 inner class 写 Provider + 缺 concrete Transport class 定义;**补丁** (1) **§5.6.3.1 新增子节**(~190 行):设计澄清(`HttpJsonRpcA2aTransport` 不是 `A2aTransport` 子接口是 concrete class,GrpcA2aTransport / InProcessA2aTransport 同理,KISS + 3 变体数量小,扳机条件 HTTP 变体 ≥ 5 才加 HttpBasedA2aTransport 子接口) + `HttpJsonRpcA2aTransport` concrete class 完整定义(implements A2aTransport 5 方法:fetchCard / submit / get / cancel / subscribe polling 占位,用 JDK 17 内置 java.net.http.HttpClient 0 额外依赖) + `HttpJsonRpcA2aTransportProvider` concrete class(name="http-jsonrpc" + priority=10) + AutoConfiguration 改写(@Bean name 唯一);(2) **§5.6.3.2 新增子节**(~150 行):「3 件套模式」明确(concrete Transport + concrete Provider + AutoConfiguration)+ `GrpcA2aTransport` stub(grpc-java + protobuf,subscribe 用 grpc streaming 更高效,体积 +5MB R-13 mitigation (d) 镜像必执行)+ `InProcessA2aTransport` stub(同 JVM 直接方法调用,InProcessA2aRegistry 单例,0 额外依赖)+ application.yml 3 Provider 同存配置示例 + 启动日志样例 + 实施期检查清单 6 条 + 关键不变项(A2aTransport 5 方法契约不变 / A2aTransportRouter 行为不变 / RemoteAgentTool 内部完全不变 / §4.7 PermissionPolicy 等兼容);(3) §0 L1 + §13 同步;(4) CLAUDE.md v1.3.22 → v1.3.23;6049 → 6377 行(+328);v1.5.29 §6.5 (2.1) 新增 `McpServerConnection` 实现示例 (心跳保活 + 指数退避重连) + §6.5 (2) `McpTransport` 同步改写 — §6.5 (2) 假设 MCP server「连上就永远连着」,生产环境 MCP server 子进程可能被 OOM 杀、stdio 僵死、SSE 反向代理超时踢线 —— Agent 进程会因 MCP server 抖动连锁崩盘,且当前文档缺 single-source-of-truth 的心跳 / 重连机制样板,Story #009 实施者只能反推 §4.10.1 错误处理边界自己设计;**根因** §6.5 (2) 原版 `McpServerConnection.start(cfg)` 是一次性同步连接 stub,没引入状态机 / 心跳 / 重连概念;v1.5.x 早期把 MCP 当「远程 Tool 注册中心」轻量集成,没考虑 24×7 长生命周期运维需求;到 v1.5.28 多 Provider 模式 + 9 Slot 体系成熟,**MCP 的「长连接」属性被放大** —— 必须补完整的生命周期管理;**补丁** (1) **§6.5 (2.1) 新增子节**(~270 行):(a) **`McpServerConnection` 接口** `extends AutoCloseable`,8 个方法(name / state / lastHeartbeatAt / listTools / callTool / onStateChange / start / close),Javadoc 明确「非 CONNECTED 状态 callTool 直接返 error 不抛异常」「重连后 listTools 重新拉不复用旧 cache」;(b) **`ConnectionState` enum** —— `IDLE / CONNECTING / CONNECTED / DISCONNECTED / RECONNECTING / FAILED` 6 态;(c) **`McpServerConnectionFactory`** —— 按 `cfg.transport()` 分派 stdio / SSE / streamable HTTP 三实现;(d) **`StdioMcpServerConnection` 完整实现**(~180 行)—— `AtomicReference<ConnectionState>` + `AtomicInteger reconnectAttempts` + `CopyOnWriteArrayList<Consumer<...>>` + daemon `ScheduledExecutorService`;`start()` 5 步(拉子进程 → initialize → initialized → tools/list → 切 CONNECTED + 启心跳);`probe()` 双探活(`process.isAlive()` + MCP `ping` 请求等回包,timeout=hbTimeoutMs);`scheduleReconnect()` 走 `1s → 2s → 4s → 8s → 16s → 32s → 60s(cap)` 指数退避,失败**无限**重试;(e) **`SseMcpServerConnection` 差异说明段** —— 3 处差异(心跳 = `GET /health` 而非 process.isAlive;重连 = 重建 `HttpClient` 而非杀子进程;长连接 = `SseEventSource` 收 server push 触发 tools/listChanged 重拉);(f) **§6.5 (2) `McpTransport` 同步改写** —— `connect()` 不再直接调 `McpServerConnection.start(cfg)`,改 `McpServerConnectionFactory.create(cfg)` + `onStateChange(listener)` + 异步 `conn.start()`;新增 `onConnectionStateChange()` 私有方法处理 `CONNECTED → register / DISCONNECTED → unregister`;(g) **配置 `application.yml` 示例 + 启动日志样例** —— github server 被 OOM 杀后重连,日志展示 tools 从 7 → 8(MCP server 升级后新增 tool 自动可见);(2) **JDK 8 兼容** —— `AtomicReference` / `AtomicInteger` / `CopyOnWriteArrayList` + `Collections.emptyList()`,**不用** `List.of` / `var` / sealed / records,与 §0 L39 硬约束对齐;(3) **关键不变项** —— `McpToolAdapter` / `ToolExecutor.dispatch()` / `PermissionPolicy.check()` 完全不变,MCP 断流在 `ToolResult` 层只表现为「error 替代 success」,**不会绕过沙箱 / 权限 / checkpoint 任何一步**,与 §4.10.1 硬规则 2 完全兼容;(4) §13 加本条目 + §0 L1 标题版本号同步 `v1.5.28 → v1.5.29`;(5) CLAUDE.md 版本号同步 `1.3.21 → 1.3.22`;**纯文档改动**,代码逻辑零改动;5709 → 6049 行(+340);v1.5.28 §5.5 改「多 Provider 模式」样板 + §5.4 同步改「唯一 Bean 名约定」+ §5.6 Slot 9 stub 同步 — v1.5.27 §5.5 用 `@ConditionalOnMissingBean` 强制「部署期二选一」,同一 Slot 最多 1 个 `XxxProvider` Bean 注册到 Spring 容器 → 用户切换 Provider 必须改 classpath / exclude / 改 Bean 名;但 §5.3.1.0 `SlotRouter<P, T>` 父类**一直是多 Provider 友好** —— 构造器收 `List<P> providers`,启动期按 `name()` 收 `Map<String, P>`,`resolve(name, cfg)` 按 name 选 → List<P> **被设计为 size=N**,而 v1.5.27 `@ConditionalOnMissingBean` 把它阉割到 size=1;v1.5.28 取消 `@ConditionalOnMissingBean` 改 plain `@Bean(name = "<slot>Provider_<name>")` + 唯一 `name()` —— 多 Provider 模式全开,Router List<P> size=N 实际生效,`agent.<slot>.name` 按名路由无需 exclude / rebuild classpath;对应 §5.4 plugin AutoConfiguration 编写约定也同步改「唯一 Bean 名约定」;5639 → 5709 行(+70);纯文档改动,代码逻辑零改动;v1.5.27 §4.6 ToolExecutor 接口定义补全 — 标题「Tool 与 ToolExecutor」但 §4.6 缺 `ToolExecutor` 本体,Story 实施者只能从 §4.10.1 / §5.5 / §6.5 散落引用反推;**补丁** 在 §4.6 `Skill extends Tool` 之后 / `ToolExecutionContext` 之前插入 `ToolExecutor` 接口完整定义 —— 单方法 `dispatch(ToolCall call, ToolExecutionContext ctx) → ToolResult`;Javadoc 覆盖 (1) 调用契约 + ReAct Action 阶段每个 `LlmResponse.getToolCalls()` 元素**必须**走此方法(不得直调 `tool.execute()`);(2) §4.10.1 硬规则 2 强制要求 —— ToolExecutor 内部统一串入 5 步流水线 `PermissionPolicy.check() §4.7 → ToolRegistry.lookup(name) → TimeoutWrap → SandboxApply(fs / http / process) §4.7 → tool.execute() → Checkpoint`,任何一步绕过 = 沙箱 / 权限 / 取消 / 超时全失效,Spring AI `ChatClient.tools().call()` 自动执行**禁止**使用;(3) ToolExecutor 与 Tool 接口解耦 —— ToolExecutor 不 import Tool 内部细节,只看 `ToolCall(name + args JSON)` + `ToolExecutionContext`,Tool 实现可手写(§6.5 (1))/ MCP server 暴露(§6.5 (2))/ Spring AI `@Tool` 注解生成仅 schema(§6.5 (3))—— ToolExecutor 一视同仁;(4) Provider 可插拔 —— 默认 `DefaultToolExecutorProvider`(stub §5.5 L2121)同步串行 dispatch;替代实现 `ParallelToolExecutorProvider`(并发)/ `ObservabilityToolExecutorProvider`(metric / trace),`name()` 走该实现标识("default" / "parallel" / "observability"),`priority()` ≥ 10 胜过默认 `priority=0`,**禁止与默认 `name()` 冲突**;(5) `@throws` 完整标注 —— `PermissionDeniedException`(§4.7)/ `ToolNotFoundException` / `TimeoutException`(`callConfig.timeoutSeconds`)/ `CancellationException`(Ctrl+C / FlowEngine markDone / 超时联动)4 类异常;**效果** Story 实施者打开 §4.6 即可看到完整 Slot 5 接口契约,无需散落反推;§4.10.1 硬规则 2 引用 `ToolExecutor.dispatch()` 现在有 single-source-of-truth 锚点;5590 → 5639 行(+49);纯文档补全,代码逻辑零改动;v1.5.26 §5.3.1.0 补齐 `FlowEngineRouter` 第 7 个隐式 Router concrete stub(extends `SlotRouter<FlowEngineProvider, FlowEngine>`,super 传 `"FlowEngine"` + Logger,Javadoc 说明**不在 SlotResolver 字段里,由 AgentFactory 直接 `@Autowired`** + 默认 `LinearTurnEngineProvider`(§6.1 L2530)+ 替代 `GoogleAdkFlowEngineProvider`(§4.11.2)/ `AlibabaGraphFlowEngineProvider`(§4.11.3));§5.3.1 标题计数 8 → 9 Router + §5.3.1.0 标题 6 → 7 隐式 Router + §5.3.1.0 总表加「注入位置」新列 + Slot 编号对齐 §5.6.4 SPI 总表(1—9);§5.6.4 SPI 总表新增「Router stub 位置」列 9 行 —— 7 行指 §5.3.1.0 + 1 行指 §5.3.1.1 + 1 行指 §5.3.1.2,Slot 8 行强调 `FlowEngineRouter` 注入 `AgentFactory` 而**不在 SlotResolver**;根因 v1.5.18 SlotResolver 屏蔽 Router 数 6 → 8 时只补 SlotResolver 内部 2 Router(MemorySource + A2aTransport),`AgentFactory.flowRouter` 是 v1.5.18 之前就已存在的字段(L3676),v1.5.23 §5.3.1.0 新增 6 隐式 Router 时漏了 `FlowEngineRouter`;**效果** Story #001 实施者打开 IDE 时,AgentFactory 启动校验所需 3 Router(`PermissionPolicyRouter` + `ToolExecutorRouter` + `FlowEngineRouter`)全部有完整 stub;v1.5.25 §5.4 末新增「plugin AutoConfiguration 编写约定(双 `@ConditionalOnMissingBean` 模式,避免双胜出)」子段 — 明确 plugin 自己的 `XxxProvider` Bean **也必须**加 `@ConditionalOnMissingBean(XxxProvider.class)`,与 §5.5 默认 AutoConfiguration 对称;Spring `@ConditionalOnMissingBean` 检查整个 `BeanFactory` 而非"当前 `@Configuration` 类内其他 `@Bean` 方法",**跨 AutoConfiguration 类的 Bean 可见性**取决于加载顺序,plugin 漏标会导致启动期 `BeanDefinitionOverrideException`(Spring Boot 2.1+ `spring.main.allow-bean-definition-overriding=false` 默认);附 (1) plugin 样例代码 `RagAutoConfiguration` 标 `@ConditionalOnMissingBean`;(2) 4 行 4 列加载顺序矩阵(plugin A 先 / plugin B 先 / 默认 / 漏标 四种情形)—— 一眼看清"漏标 = 启动失败";(3) 应急路径(第一时间检查 plugin 的 `@Bean` 是否漏标,**不要**开 `spring.main.allow-bean-definition-overriding=true` 掩盖问题);(4) 区分"plugin 注册 `XxxProvider` Bean(需双标)" vs "plugin 注册 `Tool` / `MemorySource` / `SkillSource` 等非 Slot 类型 Bean(按需创建,无需 `@ConditionalOnMissingBean`)" + `LocalToolsAutoConfiguration` 多 Tool 样例;(5) 传递依赖规则 + cross-ref §5.5 / §5.3 / §5.2;Story #009 / #016 实施期及未来 plugin 贡献者直接按 §5.4 子段样板写 plugin 即可;纯文档补全,代码逻辑零改动;v1.5.24 §5.5「默认实现的注册约定」子节扩展 — 原版只给 `DefaultPromptBuilderProvider` 1 个 `@AutoConfiguration` 模板,§5.6.4 SPI 总表 9 Slot × 默认 Provider 中只 3/9 有完整 stub(`DefaultPromptBuilderProvider` Slot 1 / `LinearTurnEngineProvider` Slot 8 / `HttpJsonRpcA2aTransportProvider` Slot 9),剩 6/9 默认 Provider 只列名未给 AutoConfiguration 样板 —— 补齐 Slot 2—7 共 6 个新 AutoConfiguration stub(`TruncatingCompactorProvider` / `AnthropicLlmProviderFactory` / `StrictPermissionPolicyProvider` / `DefaultToolExecutorProvider` / `FileSessionStoreProvider` / `ProjectClaudeMdSourceProvider`),共享 `@AutoConfiguration` + `@Bean @ConditionalOnMissingBean(<X>Provider.class)` + 匿名 inner class 模板,`create()` body 抛 `UnsupportedOperationException` 留给对应 Story 实施期填;附 (1) 9 Slot × 默认 Provider ↔ `create()` 返回类型 ↔ Story ↔ stub 位置 总表(L2118-2128 9 行),§5.6.4 与 §5.5 双向 cross-ref;(2) 10 个替代实现(`OpenAi / Gemini / DeepSeek` 3 个 LlmProvider / `Memory / Redis / Jdbc` 3 个 SessionStore / `Identity / ProjectTree / Conversation` 3 个 MemorySource / `Summary` 1 个 Compactor)追加约定 —— 模式同默认实现,`name()` 严格命名空间隔离;纯文档补全,代码逻辑零改动;v1.5.23 §5.3.1.0 新增「6 个隐式 Router concrete 类」子节 — 补齐 v1.5.18 SlotResolver 屏蔽 8 Router 时,§5.3.1.1/§5.3.1.2 只给了 2 Router 显式 stub(`MemorySourceRouter` + `A2aTransportRouter`),剩 6 个(`PromptBuilderRouter` / `LlmProviderRouter` / `ToolExecutorRouter` / `PermissionPolicyRouter` / `SessionStoreRouter` / `CompactorRouter`)只列名未给 concrete 定义;单 Java 文件给 6 Router 完整 stub,共享 `package` + `import`,附 6 Router ↔ Slot ↔ `<P, T>` ↔ Story 总表 + 5 行边界约束表 + 与 MemorySourceRouter 关键差异说明 + 实施期顺序建议(Story #001 → #002 → #003 → #014 → #015);v1.5.22 §10.1 锁定合计数显式化 + CLAUDE.md §11.6 历史 drift 修正;v1.5.21 §5.7 新增「插件机制选型决策(SPI vs ClassLoader 隔离)」子节 — 一站式回答"为什么 LingShu 用 Spring Boot SPI 而不是 OSGi / Pf4j / 自定义 ClassLoader 隔离";含决策结论 + 8 维机制对比表 + LingShu 7 个决策点场景契合度 + 主动放弃能力表 + 重新评估触发条件与演进路径;v1.5.20 §5.3.1 新增子节,补齐 v1.5.18 引入的 2 个 Router(`MemorySourceRouter` + `A2aTransportRouter`)concrete 类 stub + 通用模板 + 边界行为表;v1.5.19 §6.1 LinearTurnEngine + Provider 加类级 Javadoc;v1.5.18 §5.3 SlotResolver 屏蔽 Router 数 6 → 8;v1.5.17 §5.1 typed-Provider 列表补 `A2aTransportProvider` 行;v1.5.16 §5.1 L1463 orphan fence opener 误吞修复;v1.5.15 §4.11 Java 代码块补 closing fence;v1.5.14 §4.5.1 [TOOL SCHEMAS] 措辞修订 + 明确分层;v1.5.13 PromptBuilder API 对齐 .tools 字段;v1.5.12 删 stale Slot N;v1.5.11 Slot 命名对齐;v1.5.10 修 §0.1;v1.5.9 增 §4.5.1 [TOOL SCHEMAS] 段)
> - SpecKit SOP:[`speckit_operator_prompt.md`](./speckit_operator_prompt.md) v1.15 / 859 行 / 操作手册(R-13 mitigation (d) 镜像 + 依赖同步到 dsh v1.5.31 §5.6.3 L2429-2444 RemoteAgentTool 降级为普通 Tool 模式)
> - Prompt 速查:[`lingshu_spec_prompts.md`](./lingshu_spec_prompts.md) v1.0.14 / 368 行 / 新窗口 Prompt 模板(同步 dsh v1.5.31 §5.6.3 L2429-2444 RemoteAgentTool 降级为普通 Tool 模式)
> - SKILL:`~/.claude/skills/lingshu-spec-driven-dev/SKILL.md` v1.0.28 / 219 行 / 自动触发(R-13 dep-tree 自查链路已纳入 + 同步 dsh v1.5.33 §5.6.3.0 四个核心类型 + v1.5.34 §7.1 Agent 生命周期与扩展边界 + v1.5.32 §6.4 Skill 三个契约锚点补全 + v1.5.58 §6.1 L3639-3643 LinearTurnEngine compactor invocation 4 步 不变)
---
## 1. 项目身份
- **名称**:灵枢 LingShu Agent Engine(代号 DSH Agent)
- **定位**:JDK 8+ Java Agent 引擎,Spring Boot SPI,ReAct Loop,**9 个可插拔 Slot**
- **版本**:v0.1.0-SNAPSHOT(开发中,设计文档锁定 v1.5.6,见 §13)
- **设计灵感**:Apache DSH / Dubbo SPI 风格
- **目标用户**:企业内 AI 编码助手 / 业务配置方(只写 YAML) / 框架贡献者
---
## 2. 技术栈锁定(JDK 8 only)
| 项 | 版本 / 说明 |
|---|---|
| **编译目标** | Java 1.8(`<source>1.8</source>`)|
| **运行 JRE** | JDK 8 / 11 / 17 / 21 LTS(Spring Boot 3.2.5 实际跑需 JDK 17+)|
| **JVM 厂商** | Temurin / Zulu / Alibaba Dragonwell / IBM Semeru |
| **Spring Boot** | 3.2.5(BOM 引入)|
| **Spring AI** | `1.0.0-M6`(BOM 引入,v1.5.7 起)— **只**用于 LLM 协议转换 + `@Tool` Schema 生成,见 §11 #8 |
| **Lombok** | 1.18.30(配置类**全部** `@Value` 不可变风格)|
| **OpenTelemetry** | 1.32.x(**不跨 1.x → 2.x**,API 不兼容)|
| **Reactive Streams** | `org.reactivestreams:reactive-streams:1.0.4`(JDK 8 没内置)|
| **构建** | Maven 3.6.3+ 多模块(父 POM + 5 子模块)|
| **CI** | GitHub Actions matrix:ubuntu + JDK 8 / 17 / 21 |
---
## 3. JDK 8 硬约束(避坑清单)
- ❌ `record`(JDK 14+)
- ❌ `sealed` / `permits`(JDK 17+)
- ❌ `var` 关键字(JDK 10+)
- ❌ `List.of(...)`(JDK 9+)→ 用 `Arrays.asList(...)` 或 `Collections.unmodifiableList(...)`
- ❌ Pattern matching for switch(JDK 17+)
- ❌ Text blocks `"""..."""`(JDK 15+)
- ✅ 用 Lombok `@Value` / `@Builder` / `@NonNull` 替代 records
- ✅ 用 `org.reactivestreams:reactive-streams:1.0.4` 而非 JDK 9+ `Flow` API
---
## 4. 文件地图(本会话可见的资产)
| 用途 | 绝对路径 | 说明 |
|---|---|---|
| 设计文档 | `~/Documents/AIFullStack/MyDSHAgentDesign/dsh_agent_design.md` | **项目真理**,4768 行,**只读** |
| SpecKit SOP | `~/Documents/AIFullStack/MyDSHAgentDesign/speckit_operator_prompt.md` | SpecKit 操作手册,820 行 |
| Prompt 速查 | `~/Documents/AIFullStack/MyDSHAgentDesign/lingshu_spec_prompts.md` | 新窗口 Prompt 模板,368 行 |
| SKILL | `~/.claude/skills/lingshu-spec-driven-dev/SKILL.md` | Claude Code 自动触发 |
| SKILL mirror | `~/Documents/AIFullStack/MyDSHAgentDesign/.claude/skills/lingshu-spec-driven-dev/SKILL.md` | 拷贝版,跨机器用 |
| 主仓根 | `~/code/lingshu/`(或 clone 后的仓根)| 有 `pom.xml` / `lingshu-core/` / ... |
---
## 5. 仓目录结构(Maven 多模块,5 个 — 速查)
> **以 `dsh_agent_design.md §10` 为准**;本节只列顶层模块边界,Claude `ls` 能发现的不写。
```
lingshu/ ← 主仓根
├── pom.xml ← 父 POM(Spring Boot 3.2.5 父继承)
├── lingshu-core/ ← 核心:9 Slot 接口 + AgentConfig + 默认实现
├── lingshu-a2a-client/ ← A2A 客户端(§5.6,可独立打包)
├── lingshu-a2a-server/ ← A2A 服务端 + AgentCard 生成
├── lingshu-examples/ ← 教学示例(≤ 10 个,各 ≤ 100 行,见 dsh §10.2)
└── lingshu-cli/ ← CLI 入口(`mvn exec:java`,见 dsh §10.3)
```
**包路径**:`ai.lingshu.core.*`(lingshu-core)/ `ai.lingshu.a2a.{client,server}.*`
**模块依赖方向**:core 不依赖 a2a-*;examples/cli 依赖 core + a2a-*;**严禁反向依赖**
**每个 Story 默认改动模块**(速查):
| Story | 主要改哪个模块 |
|---|---|
| #001 zero-config-bootstrap | lingshu-core + lingshu-examples |
| #002 identity-instructions-memory | lingshu-core(`PromptBuilder` 默认实现)|
| #003 spi-slot-router | lingshu-core(`SlotRouter` 接口)|
| #009 a2a-agent-card | lingshu-a2a-server(主)+ lingshu-core(接入 `A2aTransport`)|
| #016 audit-log | lingshu-core(`AuditLogger` SPI)|
| 其它 Story | 主要 lingshu-core;详见 dsh §3.1 SOP |
---
## 6. 9 个 Slot 接口(dsh §4 — 核心骨架)
| # | Slot | 接口 | 默认实现 |
|---|---|---|---|
| 1 | LLM | `LlmProvider` | Anthropic LlmProvider(Story #003)|
| 2 | Tool | `Tool` + `ToolExecutor` | 默认 Executor(Story #004)|
| 3 | Sandbox | `Sandbox` | JVM 内 chroot(Story #001 后)|
| 4 | Skill | `SkillSource` + `Skill` | classpath + directory 双源(Story #002)|
| 5 | SessionStore | `SessionStore` | memory(Story #014)|
| 6 | Compactor | `Compactor` | 无(Story #015)|
| 7 | PromptBuilder | `PromptBuilder` | 5 段装配(Story #002)|
| 8 | FlowEngine | `FlowEngine` | LinearTurnEngine = ReAct Loop(Story #001)|
| 9 | A2aTransport | `A2aTransport` | LocalAgentCardGenerator(Story #009)|
**包命名**:`ai.lingshu.core.*`
---
## 7. 核心约定(读 CLAUDE.md 时必看)
- **配置类**:全部 `@Value` + `@Builder`,**无 setter
- **错误码命名**:`LINGS-<域><编号>`,域字母 = `C/S/L/T/X/R/A/Z`(详见 dsh §15)
- **不可变性优先**:能 `@Value` 就不用 `@Data`
- **常量**:`SCREAMING_SNAKE_CASE`
- **业务三件套**:Identity / Instructions / Memory(详见 dsh §8.1)
- **零配置原则**:空 application.yml 必须能启动,所有 27 字段有默认值
- **5 段 Prompt**:`[ROLE] / [INSTRUCTIONS] / [PROJECT MEMORY] / [CONVERSATION HISTORY] / [USER MESSAGE]`
---
## 8. 常用命令
```bash
# 环境检查
mvn -v # Maven 3.6.3+ / JDK 1.8.0_xxx+
# POM 语法校验(不下依赖)
mvn validate -N
# 编译 + 测试
mvn -pl lingshu-core -am compile
mvn -pl lingshu-core test
# 全模块编译
mvn -DskipTests=true install
# 跑某个 Story 的 AC 测试
mvn test -Dtest=AC_NN_ClassName
# 性能压测(Story #010 后才有)
k6 run perf/load/<scenario>.js
```
---
## 9. Git workflow
```bash
# 提交约定
feat(agent): Story #NNN <slug> — <一句话>
docs(design): vX.Y.Z — <一句话>
fix(slot-<N>): <一句话>
chore: <一句话>
# 不允许的提交
× 直接 commit 到 main
× 跨多个 Story 的"feat:" commit
× 没跑 AC 的 PR
× 把 SOP / Prompt 速查 / SKILL push 到 lingshu 仓
```
---
## 10. 性能预算(dsh §14.15.1,实施时对齐)
| 指标 | 目标 |
|---|---|
| LLM 流式首 token | P50 ≤ 1.5s / P99 ≤ 3.0s |
| turn 完成(10 steps)| P50 ≤ 30s / P99 ≤ 60s |
| Tool 调用 | P99 ≤ toolTimeoutSec |
| 单 turn history | ≤ 100K tokens |
| 冷启动 | ≤ 30s(空 yml)|
| 并发 turn 数 | 默认 16,排队 ≤ 32 |
---
## 11. 硬约束(违反即 reject)
1. **不 push SOP 到 lingshu 仓**(`speckit_operator_prompt.md` / `lingshu_spec_prompts.md` 是本地流程制品)
2. **不省略 AC 黑盒验证**(每个 Story 必须跑对应的 AC-NN 才能合)
3. **不跨 Story 改 constitution**(改宪章必须走 RFC 流程,先开 issue + 评审)
4. **Story 边界**:≤ 5 个核心文件改动,≤ 3 个 ErrorCode 引入(超过就拆)
5. **不绕过 Spring Boot SPI**(新增 Slot 必须走 Provider + SlotRouter,**不要硬编码**)
6. **不引入额外依赖**(dsh §10.1 已锁 13 项[含 `spring-ai-bom`,v1.5.7 引入],新依赖需 RFC + `dependency:tree` CI 卡点 + **`banned-dependencies` enforcer build 阶段 fail**(见 dsh §17 R-13);任何 Story 实施者必须按 SOP §3.2 AC-NN-deps-* + §3.4 T-dep-tree-* 流程自查后提交,**PR body 末尾**必须有 `### R-13 dependency:tree 自查` 节)
7. **ReAct Loop 必须自实现**(不得用 Spring AI `ChatClient.prompt().call()` 自动工具执行;核心循环 ~ 数十行,完整掌握 Agent 工作机制,保留定制循环行为的空间 — dsh §4.10.1 硬规则 1)
8. **Spring AI 只用两件事**:① LLM Provider 协议转换(OpenAI / Anthropic / Gemini / DeepSeek / Qwen / Kimi 等格式差异) ② `@Tool` 注解 JSON Schema 生成。**必须禁用** Spring AI 自动 tool 执行 — 会绕过 ToolExecutor 的沙箱/权限/checkpoint,导致 tool 被调两次(dsh §4.10.1 硬规则 2)
9. **Provider 必须显式映射**:多 `ChatModel` 并存时 Bean 类型相同,必须维护 `Map<String, ChatModel> providerMap` 显式查找;不得靠 Spring 容器扫 Bean 类型区分(dsh §4.10.1 硬规则 3)
---
## 12. 常用反问(避免无脑实现)
- 用户说"实现 X" → 先问"对应哪个 Story?或对应哪条 AC?"
- 用户说"改 Y" → 先确认 Y 是否在已有 Story 范围,避免 Scope creep
- 用户说"加依赖 Z" → 先查 dsh §10.1 锁定表
- 用户说"用 record/sealed/var" → 提醒 JDK 8 约束
- 用户说"push SOP" → 提醒归档边界
---
## 13. 项目节奏(KPI)
- **Story 完成率**:每周 / 每月闭合几个 Story
- **平均 AC 通过率**:第一遍跑过的比例(目标 ≥ 80%)
- **宪章稳定性**:`constitution.md` 一个月改几次(目标 0 次,改必须 RFC)
- **Risk Register 缓解率**:R-XX 中已缓解的比例(目标 ≥ 60% by v1.0 GA)
---
- **Last updated**: 2026-10-14
+ **Last updated**: 2026-10-16
+ **Version**: 1.3.58(v1.3.58 Story #049 agent-run-live-streaming 实施 + 文档同步 — dsh v1.5.60 → v1.5.61(§0 标题 v1.5.60 → v1.5.61 + §13 changelog 新增 v1.5.61 行 + §6.1 段补 `DefaultAgent.run()` `LiveRunPublisher` 5 段状态机 + 反压 + cancel-safe parkNanos 🆕 v1.5.61 Story #049 标记)+ **Story #049 agent-run-live-streaming 实时流式真接通** —— `DefaultAgent.run(Prompt) → Publisher<AgentEvent>` hot-lazy reactive publisher 替代 Story #001 drain-and-replay 模式,首个事件延迟从 O(turn) ≈ 数百 ms ~ 数秒降到 O(emit) ≈ 数 ms,直追 Claude Code 体感;**Reactive Streams 1.0.4 spec 严格合规 5 段** —— §2.2 单订阅者(`subscribed` AtomicBoolean 守)/ §2.7 串行 onNext/onError/onComplete(`engineThread` AtomicReference 单线程串行)/ §3.9 `request(long n)` 非正值抛 IAE(`n <= 0` 立即 throw)/ §1.3 `cancel()` 终态(`cancelled` AtomicBoolean + LockSupport.unpark)/ §1.9 null check(`Prompt == null` IAE + `Subscriber == null` NPE upstream);**`DefaultAgent.LiveRunPublisher` 5 段状态机** —— `subscribed: AtomicBoolean` / `requested: AtomicLong` / `cancelled: AtomicBoolean` / `sub: AtomicReference<Subscription>` / `engineThread: AtomicReference<Thread>` + 2 私有常量 `PARK_NANOS = 50_000_000L` (50ms 反压 sleep) + `PARK_TIMEOUT_NANOS = 1_000_000_000L` (1s cancel 检查窗口);**`onEvent(AgentEvent)` engineThread 5 段决策** —— (1) subscribed=false 直接 return (2) cancelled=true 直接 return (3) sub=null 直接 return (4) EngineThread 自己 lock + 数据可消费 → 累加 delivered + 检查 cancelledState → drained OK → `sub.request(1)` 通知 subscriber 消费 (5) drain() subscriber 端循环:取一个 event `delivered >= requested` LockSupport.parkNanos(PARK_NANOS) + `cancelled.get()` 检查 / `parkNanos(PARK_TIMEOUT_NANOS)` cancel-safe 唤醒;**`runBlocking(Prompt) → LlmResponse` 100% 同步路径保留 reverse AC 4 case 守护**(只新增 `run()` 单方法接口契约,老 `runBlocking()` 完整 100% 保留 4 反向 AC test 守不退步 —— 这是「不动存量路径」Story 的核心约束,加 publisher 不拆 drain-and-replay 通路);**11 net new case** 跨 2 文件(`DefaultAgentLiveStreamingTest` 7 L2/L3:`run_replaysAllEventsFromTurnStart_andClosesPublisher` happy path 收到 TurnCompleted + close / `run_letsSubscriberControlBackpressure_throughRequestN` request(1) 阻塞 + request(N) 解锁 / `run_respectsCancelMidStream_andStopsEmittingEvents` cancel 后不再 emit + publisher.close / `run_throwsOnNegativeRequest` §3.9 / `run_throwsOnNullPrompt` §1.9 / `run_replaysBackwardCompat_withRunBlocking` reverse AC subscribe 后同时 runBlocking 收同样 event list / `run_failsOnTerminalStateAfterException` onError 后 close + `executor.isShutdown()` 兜底)+ `DefaultAgentRunBlockingUnchangedTest` 4 L1 reverse AC(`runBlocking_returnsFinalAssistantText` / `runBlocking_emitsEventsInOrderViaSink` / `runBlocking_preservesThreadContract` / `runBlocking_doesNotRequireSubscriber`);**780 全 lingshu-core 测试 PASS** (769 pre-#049 chain 0 改动 + 11 新 case);R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree -Dverbose` md5sum diff 仅时间戳差异 = `eb0218222ba492c9ac1f13e5f5d7dcdf → 45bff3d222f30914bdbf681615b34147` = **0 binary delta 第 32 次 PASS**(纯 JDK 8 `AtomicBoolean.compareAndSet` / `AtomicLong.addAndGet` / `AtomicReference.compareAndSet` / `LockSupport.parkNanos` / `LockSupport.unpark` / `ExecutorService` / `ThreadFactory` / `ReentrantLock` + Awaitility testing 已锁 13 项依赖表内 0 新 binary 引入);`banned-dependencies` enforcer Rule 0 passed;**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 JDK `NullPointerException` + `IllegalArgumentException` + `IllegalStateException` 抛 upstream,Reactive Streams spec §1.9 / §3.9 不增加 ErrorCode);**关键不变项** —— `Agent` 接口契约不变(`run(Prompt) → Publisher<AgentEvent>` 是新单方法接口契约,`runBlocking(Prompt) → LlmResponse` 不变兼容)/ `DefaultAgent` 公开方法签名不变(只新增 `run(Prompt)`,老 `runBlocking(Prompt)` 完整 100% 保留 reverse AC 守)/ `LinearTurnEngine` 不变 + 9 Slot 体系 100% 真接通不变 + 26 字段 AgentConfig schema 不变 / `JdkIncompatibleClassChangeError` 守住(runtime = JDK 17+,compile = JDK 8,`--release` flag 配上)/ Reactive Streams 1.0.4 §1.3/§1.9/§2.2/§2.7/§3.9 五段严格守 + Reactive Streams 1.0.4 spec 严格合规;Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住) + JDK 8 兼容(`AtomicBoolean` / `AtomicLong` / `AtomicReference` / `LockSupport.parkNanos` / `LockSupport.unpark` / `ExecutorService` / `ThreadFactory` / `ReentrantLock` + `Awaitility` testing 已锁,no `var` / `List.of` / sealed / records);**累计 51 个 Story 合入**(本 Story 实施后);**业务价值** —— **`DefaultAgent.run()` 实时流式真接通**(Story #001 落地时 `runBlocking()` + 内嵌 `AgentEventSink` drain-and-replay 是 KISS but 同 turn 等 LLM 跑完才一次性写 sink,生产 demo SSE 流式用户体验从「等完成后一次刷新」变「逐 token 推进」直追 Claude Code 体感)/ **Reactive Streams 1.0.4 spec 严格合规 5 段**(后续加 subscribe-then-reactor 适配(Story #051+ reactor-core-bridge)零成本接入)/ **`runBlocking()` 100% 同步路径保留 reverse AC 4 case 守护**(Story #001 落地以来 50 个 Story 全部用 `runBlocking()` 路径,本 Story 新增 `run()` 但 `runBlocking()` 完整 100% 不动,reverse AC test 守住不退步)/ **0 新 Maven 依赖 / 0 新 ErrorCode**(纯 JDK 8 `LockSupport` + `AtomicBoolean`/`AtomicLong`/`AtomicReference` + Reactive Streams 1.0.4 spec 已锁 + Awaitility testing 已锁,R-13 (d) baseline 镜像 md5sum diff 仅时间戳差异 = 第 32 次 PASS,累计 32 个 R-13 (d) PASS 故事无新增 binary);**修复者** Claude Code(根据用户 2026-10-15 会话反馈「`DefaultAgent.runBlocking()` 内嵌 Sink 的 drain-and-replay 模式让生产 demo SSE 流式首 token 延迟 N×100ms,加一个 `run(Prompt) → Publisher<AgentEvent>` 走 Reactive Streams 1.0.4 spec 反压 + cancel-safe」+「实施吧」,触发本 Story 实施 + 11 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 32 次 PASS 0 binary delta**);constitution §10 R-13 缓解 Story 列表补 `#049` 行**第 32 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 31 → **32 个 Story**;dsh §0 标题 v1.5.60 → v1.5.61;dsh §13 新增 v1.5.61 行(本条);「对应设计文档」版本号 v1.5.60 → v1.5.61)
+
**Version**: 1.3.57(v1.3.57 Story #047 + #048 实施 + 文档同步 — dsh v1.5.59 → v1.5.60(§0 标题 v1.5.59 → v1.5.60 + §13 changelog 新增 v1.5.60 行 + §6.1 L3670-3680 LinearTurnEngine 段补 sentinel `ToolResult("[PENDING_APPROVAL] Awaiting user decision for tool: <name>")` 🆕 v1.5.60 Story #048 标记 + §4.6 `DefaultTurnContext.appendToolResult` 段补 upsert-by-toolUseId `🆕 v1.5.60 Story #048` 标记 + §6.1 LinearTurnEngine 段补 `agentFactory.create(cfg)` resolve `cfg.getPermissionPolicy()` → router.resolve 接线 🆕 v1.5.60 Story #047 标记)+ **Story #048 ask-user-sentinel-tool-result Hang 安全防护** —— Story #030 ask-mode 落地后 LinearTurnEngine.dispatchWithPolicy 在 `AskUserPermissionPolicy` 命中时调 `ApprovalRegistry.complete()` 阻塞等 `CompletableFuture<Decision>.get()` 解阻塞,但 Anthropic 协议硬约束:每个 `message_start` 内的 `tool_use` blocks 必须紧跟同数量的 `tool_result` blocks,**否则下轮 LLM 调用 HTTP 400 "messages.X: tool_use ids were found without tool_result blocks"** —— hang 期间如果用户隔夜审批 / 浏览器关闭 / demo-product crash,history 永远停在 "Assistant(tool_use) → ???" 无 tool_result 补齐,下轮 prompt 拼回去 100% 触发协议错;**根因** Story #030 inline 路径未在阻塞前 commit 一个占位 tool_result;**补丁** sentinel `ToolResult` 在 `dispatchWithPolicy` AskUser branch 检测 `permissionPolicy.check()` 返 Decision.AskUser 后,**先 commit 一个 `[PENDING_APPROVAL] Awaiting user decision for tool: <name>` 的 `ToolResult.error(...)` 到 history**(走 `turnContext.appendToolResult(toolResult)`)再阻塞 `future.get()`,确保 history 永远协议一致;**`DefaultTurnContext.appendToolResult` upsert-by-toolUseId 语义** —— 沿 history tail 向后扫描 1 个 `Message.Assistant`(其中含 `toolCalls[i].id == toolResult.toolUseId`),命中则 in-place `messages.set(idx, new Message.ToolResult(toolUseId, content))` 替换 / 不命中则 `messages.add(new Message.ToolResult(toolUseId, content))` 追加,确保重试场景下 sentinel toolResult 不重复堆叠;**`LinearTurnEngine.dispatchWithPolicy` 7 段改造** —— (1) 检测 `Decision.AskUser` (2) 构造 sentinel `ToolResult.error("[PENDING_APPROVAL] Awaiting user decision for tool: " + toolCall.name())` (3) `turnContext.appendToolResult(toolCall.id(), sentinelResult)` upsert (4) 发 `AgentEvent.ApprovalRequired` (5) `ApprovalRegistry.register(future)` (6) `future.get()` 阻塞 (7) 阻塞解后 send `LinearTurnEngine` 正常 LlmResponse 续接;**`DefaultToolExecutorProvider` 0 改动** —— `ToolExecutor.dispatch()` 5 步流水线不变,sentinel 在 LinearTurnEngine 内部生成不经过 ToolExecutor;**4 new case** 跨 2 文件(`LinearTurnEngineAskUserNoRenderIT` 4 L2:Anthropic protocol invariant — history 永远 `Assistant.toolCalls.size() == ToolResult.size()` matching by toolUseId + sentinel 一次性 commit + upsert 替换不重复堆叠 + 解阻塞后 LinearTurnEngine 正常继续 + 5 fixture 同步 site:`LinearTurnEngineE2ESmokeTest` / `LinearTurnEngineAskUserTest` / `LinearTurnEngineProviderCompactionTest` / `LinearTurnEngineProviderSessionStoreTest` / `LinearTurnEngineProviderTest` 加 sentinel-aware assertion);**766 tests pass / 0 fail / 2 MCP heartbeat flake pre-existing** / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree -Dverbose` md5sum diff 仅时间戳差异 = **0 binary delta 第 31 次 PASS**(纯 JDK 8 `CompletableFuture` + `Collections.emptyList()` + `Arrays.asList()` + Lombok `@Value` + `Message.@JsonTypeInfo`/`@JsonSubTypes` Story #014 已锁 0 新 binary 引入);`banned-dependencies` enforcer Rule 0: passed;**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 Story #030 `LINGS-P02` 仅在 `approvalTimeoutSeconds > 0` 时触发);**关键不变项** —— `PermissionPolicy` SPI 公开方法签名不变(`check(Tool, ToolExecutionContext) → Decision` 不变,只 LinearTurnEngine 内部处理 AskUser branch 变更)+ `Decision.AskUser` 3rd outcome 不变 + `ApprovalRegistry` @Component ConcurrentMap 不变(Story #030 已落)+ `AgentEvent.ApprovalRequired` 3-arg ctor 不变(Story #030 已落)+ `AskUserPermissionPolicy` 5 段决策不变(Story #030 已落,本 Story 不变 policy 逻辑,只改 LinearTurnEngine 阻塞前后的 history commit 顺序)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `AgentFactory` SPI 不变(9-Router ctor 不动,本 Story 不涉及 Router 改动)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `Message` 4 子类契约不变(只 `appendToolResult` upsert-by-toolUseId 内部语义新增,字段 shape 0 改动)+ `Tool` SPI 不变 + §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + 24 字段 AgentConfig schema 不变 + JDK 8 兼容(`CompletableFuture` + `Collections.emptyList()` + `Arrays.asList()` + Lombok `@Value` + Jackson 已锁,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**累计 49 个 Story 合入**(实施后);**业务价值** —— **Anthropic 协议层硬约束 Hang 安全防护**(hang 期间 / 用户隔夜审批 / 浏览器关闭 / demo-product crash → history 永远协议一致,下轮 prompt 拼回去 100% 通过 Anthropic validator,**零 HTTP 400 风险**)/ **`DefaultTurnContext.appendToolResult` upsert-by-toolUseId 通用语义补全**(原 append-only 语义重试时 sentinel 会重复堆叠,新 upsert-by-toolUseId 兼容 retry + multi-tool-call 并发场景)/ **demo-product + Anthropic 用户实测可工作**(sentinel 占位让 history 永远合法 → 解阻塞后 LinearTurnEngine 正常 LlmResponse 续接最终回答 user,不再因 protocol invariant 失败 400 hang);**修复者** Claude Code(根据用户 2026-10-14 会话反馈「ask-mode hang 期间如果 Anthropic SDK reject 是 protocol invariant 错的,history 永远停 tool_use 不补 tool_result,下轮 prompt 拼回去 100% 400,补一下 hang-safety」+「实施吧」,触发本 Story 实施 + 4 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 31 次 PASS 0 binary delta**);**Story #047 ask-policy-end-to-end-wiring 2 silent-ignored wiring gap 真接通** —— demo-product 实测发现 2 处 ask-policy 配置看似通过但实测零效果:`(1) AgentFactory.java:280 typo getSandbox().getPolicy() → 正确应 getPermissionPolicy()(Slot 4 字段名漂移,Slot 3 `RuntimeSandbox.getPolicy()` 与 Slot 4 `getPermissionPolicy()` 在 v1.5.0 不同 Slot 时方法名相似导致笔误);(2) demo-product ChatController.createSession 28-arg `AgentConfig.builder()` hand-roll 完全 bypass `application.yml` 的 `permission-policy: ask` + `tools.ask-list: [bash]` 配置(@Bean 注入的 AgentConfig 没被用 → Spring 容器 resolve Slot 4 Provider 时拿到 `allow` 而非 `ask`,demo-product 浏览器点 "Run bash" 实测直接 deny 不弹 approval modal)—— 2 bug 落地后 demo-product ChatController 改走 `agentFactory.create(loadYaml)` 复用 AgentFactory 7 项校验 + `Routers.PermissionPolicyRouter.resolve("ask", cfg)` + `LinearTurnEngine.dispatchWithPolicy` AskUser branch 真接通;**8 new case** 跨 3 文件(`AgentFactoryAskPolicyRoutingIT` 3 L2 yml `permission-policy: ask` 真 binding 到 Slot 4 + `Routers.PermissionPolicyRouter.byName Map size=3`(default + strict + ask) + `AgentConfig.permissionPolicy` 字段 resolve "ask" 名 + `AgentFactoryAskPolicyYamlIT` 3 L2 yml 全链路 + demo-product `application.yml` integration + 4 fixture sync site:LinearTurnEngineE2ESmokeTest / LinearTurnEngineProviderTest / LinearTurnEngineAskUserTest / LinearTurnEngineProviderSessionStoreTest + `DemoProductAskPolicyEndToEndIT` 1 L3 demo-product ChatController end-to-end 实跑 yml + browser SSE round-trip + human allow/deny decision);**766 tests pass / 0 fail / 2 MCP heartbeat flake pre-existing** / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core,lingshu-examples/demo-product dependency:tree -Dverbose` md5sum diff 仅时间戳差异 = **0 binary delta 第 31 次 PASS**(纯 Java 文件内 typo 修复 + `agentFactory.create(loadYaml)` 重写 ChatController.createSession ~50 行 + Lombok `@Value` + Spring `@Autowired` + Jackson `@JsonProperty` kebab-case + `PermissionPolicyRouter` 走 v1.5.28 多 Provider 模式 + yml `permission-policy: ask` top-level camelCase 绑定 + `R-13` mitigation (d) baseline mirror pre/post md5sum `ca0d484149a2e0deeab5eaacc5828d31 → dbefab36f933b1ea3ab50e19d8df2ba5` 仅时间戳差异 已锁 13 项依赖表内 0 新 binary 引入);`banned-dependencies` enforcer Rule 0: passed;**0 新 Maven 依赖** / **0 新 ErrorCode**;**关键不变项** —— `AgentFactory` SPI 不变(9-Router ctor 不动,只 typo 修复 + `cfg.getPermissionPolicy()` 接线,公开方法签名 0 改动)+ `PermissionPolicy` SPI 公开方法签名不变(`check(Tool, ToolExecutionContext) → Decision` 不变)+ `Decision` 3 子类(Allow / Deny / AskUser)不变 + `PermissionPolicyProvider` SPI 不变(`name()` / `priority()` / `version()` / `create(AgentConfig)` 0 改动)+ `PermissionPolicyRouter` 行为按 `name()` 路由不变(v1.5.28 多 Provider 模式 `default` / `strict` / `ask` 三 Provider 共存,yml `permission-policy: ask` 真 binding 走 `AskUserPermissionPolicyProvider`)+ `AgentConfig.permissionPolicy` 字段不变(top-level camelCase YAML key 直接 binding)+ `AskUserPermissionPolicy` 5 段决策不变(Story #030 已落,本 Story 不变 policy 逻辑,只修 AgentFactory wiring)+ `LinearTurnEngine` 公开签名不变(本 Story 不涉及 LinearTurnEngine 改动,那是 #048 的 scope)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `Tool` SPI 不变 + §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + 24 字段 AgentConfig schema 不变 + JDK 8 兼容(Lombok `@Value` + Spring `@Autowired` + Jackson `@JsonProperty` kebab-case + `PermissionPolicyRouter` v1.5.28 多 Provider 模式已锁,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**累计 50 个 Story 合入**(实施后,#047 + #048 双 Story 合入);**业务价值** —— **demo-product ask-mode 端到端真接通**(Story #030 后端 ApprovalGate SPI + Story #041 DefaultApprovalGate 抽 SPI + Story #042 frontend button UI + Story #047 yml `permission-policy: ask` 真 binding + Story #048 hang-safety sentinel = 完整 6 件套链路,browser 实测 allow/deny 通过 Anthropic validator 不再 400)/ **2 silent-ignored wiring gap 真接通**(Slot 4 typo + 28-arg hand-roll ctor bypass 是 demo-product 唯一 2 处 silent-ignored 配置坑,本 Story 100% 收口)/ **R-04 privilege escalation 缓解完整链路收口**(分值 8 —— Story #029 + #030 + #031 + #041 + #042 + #047 + #048 = 完整 ask-mode 可工作防护链路)/ **yolo back-compat**(`permission-policy: default` 仍走 `AllowAllPermissionPolicy` 不变 + demo-product `application.yml` 0 改动)/ **Future RFC 兼容**(未来加新 PermissionPolicy Provider 仅需 `providerMap` 加 entry + yml `permission-policy: <new-name>` 不改 Spring config);**修复者** Claude Code(根据用户 2026-10-14 会话反馈「demo-product 浏览器点 Run bash 实测直接 deny 不弹 approval modal,但 demo-empty 应用 `permission-policy: ask` 是好的,这是 yml binding 还是 wiring 错了?」+「实施吧」,触发本 Story 实施 + 8 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 31 次 PASS 0 binary delta**);v1.3.56(v1.3.56 Story #046 session-store-save-after-turn 实施 + 文档同步 — dsh v1.5.58 → v1.5.59
**Version**: 1.3.56(v1.3.56 Story #046 session-store-save-after-turn 实施 + 文档同步 — dsh v1.5.58 → v1.5.59(§0 标题 v1.5.58 → v1.5.59 + §13 changelog 新增 v1.5.59 行 11 节 + §6.1 L3669-3670 LinearTurnEngine 段补 `trySave(ctx)` 🆕 v1.5.59 Story #046 标记 + §5.6.4 SPI 总表 Slot 5 SessionStore row 补 `Router stub 位置: §5.3.1.0`)+ **Slot 5 SessionStore SPI save-after-turn 真接通** —— Story #014 落地只完成 Slot SPI + `InMemorySessionStore` + `FileSessionStore` + `SessionStoreProvider` + `SessionStoreAutoConfiguration` + `SessionStoreRouter` 6 件套,但 `AgentFactory.create()` resolve 后 bean 直接丢 + `DefaultAgent` 无 sessionStore 字段 + `LinearTurnEngine` 永不改 `save()` → yml `session-store: file` 配置看似通了但实测零落盘(directory 永为空);实测发现 2026-10-13 demo-product 启动 + 浏览器建立 session + 聊天后,`/var/folders/.../T/lingshu-sessions/` 目录为空 —— 本 Story 收口;`NullSessionStore.java` sentinel (`SessionStore` SPI 空实现,`save(Checkpoint)` no-op 永不出错 + `load`/`delete`/`list` 也 no-op)保护 Story #001-#014 历史 fixture(`sessionStoreRouter == null` 退路);`LinearTurnEngine` 8-arg ctor 新增 `SessionStore sessionStore` final 参数 + `trySave(TurnContext ctx)` 私有守卫(swallow `RuntimeException` 永不让 I/O 失败 crash 已 emit `TurnCompleted` 的 turn)+ 4 个 turn-completion 完整钩子点全覆盖(L186 normal-end finish / loop-head cancel 守卫 / maxSteps guard 守卫 / outer `catch (Throwable)` 守卫 + waitForLlm cancel tail branch = 5 处总触发)+ `AgentEvent.TurnCompleted` 不变 + `AgentEvent.SessionSaved` 1 段 event 子类发射;`LinearTurnEngineProvider` 8-arg ctor 新增 `Routers.SessionStoreRouter sessionStoreRouter` final 参数 + **Spring 6 dual-ctor `@Autowired` 模式** (legacy 7-arg ctor 委托 8-arg ctor 传 `null` = NullSessionStore + primary 8-arg ctor 加 `@Autowired` 显式标记,镜像 Story #045 `CompactorRouter` precedent);`AgentFactory` 8-Router → 9-Router ctor(`@Autowired SessionStoreRouter` precedent 镜像,+ `SessionStoreRouter` 在 SlotResolver 不在 `AgentFactory` 直接 @Autowired,镜像 §5.3.1.0 第 9 隐式 Router concrete stub) + `agentFactory.create(cfg)` resolve `cfg.getSessionStore()` → router.resolve(name, cfg) → wire into LinearTurnEngine 8-arg ctor;`Routers.SessionStoreRouter.resolve("file", cfg)` 名 resolution 接入 `agent.session-store: file` top-level camelCase YAML key(dsh §5.3.1.0 Slot 5 SPI);**e2e 落盘契约** —— `agent.session-store: file` 配置下完整 turn 后断言 `Files.list(sessionStoreDir).count() == 1` + JSON 内容含 `history.length > 0`(`Checkpoint.@JsonTypeInfo` Jackson 多态 round-trip `Message` 4 子类 + `history: List<Message>` 序列化/反序列化 — Story #014 已落 `Checkpoint.@Value` + `@JsonCreator` + `@JsonProperty` 4-arg ctor + `Message.@JsonTypeInfo` + `@JsonSubTypes`);**16 net new case** 跨 5 测试文件(`NullSessionStoreTest` 2 L1 null-safety + history 守恒 + `RecordingSessionStoreTest` 2 L1 records save + throwOnSave 注入 + `LinearTurnEngineSessionStoreTest` 6 L1 5 turn-completion sites 覆盖 + save failure swallowed + `LinearTurnEngineProviderSessionStoreTest` 3 L1 8-arg ctor router.resolve + legacy 6/7-arg 双 back-compat + `AgentFactoryYamlSessionStoreSaveIT` 3 L2 e2e `Files.list(dir).count() == 1` 落盘验证 + `AgentFactoryYamlSessionStoreLoadIT` 2 L2 full-slot parse + unknown-name parse-through);**全 761 tests pass / 0 fail / 2 MCP heartbeat flake pre-existing** / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree` diff **仅时间戳差异** = **0 binary delta 第 30 次 PASS**(纯 JDK 8 `Lombok @Value` + `Spring @Component` + `Autowired(required=false)` + `Nullable` + `LingsSessionStoreException[LINGS-X01]` 复用 Story #014 + `FileSessionStore` atomic file write Story #014 已锁 + Jackson `@JsonTypeInfo/@JsonSubTypes/@JsonCreator/@JsonProperty` Story #014 已锁 + `Files.list` + `Files.write` + `Path.resolve` JDK 8 已锁 0 新 binary 引入);`banned-dependencies` enforcer Rule 0: passed;**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 Story #014 `LINGS-X01` SESSION_STORE_IO_FAILED);**关键不变项** —— `SessionStore` SPI 公开方法签名不变(`save(Checkpoint)` + `load(String) → Optional<Checkpoint>` + `delete(String)` + `list() → List<String>` + `CONTRACT_VERSION="1.0.0"` 不变,只新增 1 个 Null Object 实现) + `LinearTurnEngine` 公开方法签名不变(只新增 1 个 ctor 参数 + `AgentEvent.SessionSaved` event 子类内部发射,不引入新公开方法) + `AgentFactory` SPI 不变(8-Router ctor → 9-Router ctor 只 +1 Router 字段,公开方法签名 0 改动) + `AgentConfig` 不可变契约不变(0 字段新增 —— `agent.session-store` 顶层 key 直接 binding 到 `cfg.getSessionStore()` string) + `Message` 4 子类契约不变(Story #014 Jackson 注解已落 + Story #046 v1.3.41 删除 `Message.ToolUse` 死代码已落) + `Checkpoint.@Value` 4-arg ctor 不变(Story #014 已落) + `Tool` SPI 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2) + §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系 100% 真接通 + JDK 8 兼容(`Lombok @Value` + `Spring @Component` + `Autowired(required=false)` + `Nullable` + `ConcurrentHashMap` + Jackson 已锁,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**累计 47 个 Story 合入**(实施后);**业务价值** —— **`Routers.SessionStoreRouter` Javadoc 自陈「full save/load wiring into the Agent loop is the work of a follow-up Story」known gap 关闭**(Story #014 落地后已知预留,本 Story 100% 兑现)/ **Null Object pattern 保护历史 fixture**(`NullSessionStore.INSTANCE` wire-in `SessionStore` ctor 为 null 时,Story #001-#014 fixture 0 改动全过 — `LinearTurnEngine` 6-arg 老 ctor 委托 7-arg 委托 8-arg 传 null 测试 fixture 0 改动全过)/ **Spring 6 dual-ctor 实战验证 Plan B 二次**(legacy 7-arg ctor + primary 8-arg ctor + `@Autowired` 显式标记,后续 #047+ 新增 SPI 时直接复用此模式)/ **SessionStore 名 resolution 接入 dsh §5.3.1.0**(同 `PermissionPolicyRouter` / `ToolExecutorRouter` / `FlowEngineRouter` / `CompactorRouter` precedent,业务方 yml 写 `agent.session-store: <name>` 即可热切)/ **v0.1.0 GA 9 Slot 真接通**(`LinearTurnEngine.trySave` 5 turn-completion sites 覆盖,持久化路径从引擎主循环到磁盘完成 round-trip,demo-product 浏览器聊天后 `Files.list(dir).count() == 1` 真落盘)/ **Checkpoint Jackson round-trip 实战验证**(`Files.list` 后的 `.json` 文件含 `history` 数组,加载回内存 = `Checkpoint.sessionId` + `history.length > 0` + 完整 `Message` 多态序列);README.md 「更新日期」段加 🆕 v1.5.59 Story #046 顶部 blockquote + 「核心特性」段补 💾 Slot 5 SessionStore save-after-turn 真接通 bullet + 「Story 路线图」段追加 #046 retrospective(`NullSessionStore` sentinel + `LinearTurnEngine` 8-arg ctor + `LinearTurnEngineProvider` 8-arg ctor + Spring 6 dual-ctor `@Autowired` 模式 + `AgentFactory` 9-Router ctor + `SessionStoreRouter` resolve + 16 case AC 黑盒验证 / 全 761 tests pass / R-13 0 binary delta 第 30 次 PASS / 0 新 ErrorCode / 累计 47 个 Story);specs/ROADMAP.md 段一 ✅ 已完成加 #046 行(2026-10-13,761 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 30 次 PASS / 0 新 ErrorCode);**修复者** Claude Code(根据用户 2026-10-13 会话反馈「实施吧」+ Story #046 spec/plan/tasks 已审批通过,触发本 Story Phase 1-5 实施 + 16 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 30 次 PASS 0 binary delta**);constitution §10 R-13 缓解 Story 列表补 `#046` 行**第 30 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 29 → **30 个 Story**;dsh §13 v1.5.59 行新增 11 节记录本 Story 完成 + dsh §6.1 L3669-3670 LinearTurnEngine 段补 trySave 4 步 `🆕 v1.5.59 Story #046` marker + §5.6.4 SPI 总表 Slot 5 SessionStore row「Router stub 位置」字段补 +「对应设计文档」版本号 v1.5.58 → v1.5.59);1.3.55(v1.3.55 Story #046 session-store-save-after-turn spec/plan/tasks ready — 0 行代码改动 / 0 行 dsh 改动;**Story #046 spec/plan/tasks 三件套落地**(2026-10-13)+ **§6 主链 Slot 5 SessionStore save-after-turn wiring 缺口 spec 收尾** —— Story #014 落地只完成 Slot SPI + `InMemorySessionStore` + `FileSessionStore` + `SessionStoreProvider` + `SessionStoreAutoConfiguration` + `SessionStoreRouter` 6 件套,**未把 `DefaultAgent` / `LinearTurnEngine` 改成「turn 完成 → `session.checkpoint()` + `sessionStore.save(...)`」**;实测发现 2026-10-13 demo-product 启动 + 浏览器建立 session + 聊天后,`/var/folders/.../T/lingshu-sessions/` 目录为空;`Routers.SessionStoreRouter` Javadoc 自陈「full save/load wiring into the Agent loop is the work of a follow-up Story」,是已知 gap 不是新 bug;**Story #046 spec/plan/tasks 设计意图**:**3 prod file change**(NullSessionStore sentinel ~40 行 + `LinearTurnEngine` 8-arg ctor + 4 个 turn 收口点 `trySave()` 守卫 + `LinearTurnEngineProvider` 8-arg ctor + dual-ctor + `AgentFactory` 9-Router ctor @Autowired SessionStoreRouter)+ **5 test files**(`NullSessionStoreTest` 2 L1 + `RecordingSessionStoreTest` 2 L1 + `LinearTurnEngineSessionStoreTest` 6 L1 + `LinearTurnEngineProviderSessionStoreTest` 3 L1 + `AgentFactoryYamlSessionStoreSaveIT` 3 L2 e2e turn 后磁盘有文件 + `AgentFactoryYamlSessionStoreLoadIT` 2 L2)= **3 prod + 5 test = 8 文件 / 16 net new case**;**镜像 #045 Slot 6 Compactor wiring precedent 100% 对位** —— #045 = `LinearTurnEngine` 7-arg + `LinearTurnEngineProvider` 7-arg + `AgentFactory` 9-Router + `NullCompactor` sentinel + `CompactorRouter` resolve(`compactor.compact(history, toolResult)` in 4 turn 收口点),**#046 = 8-arg + 8-arg + 9-Router + NullSessionStore sentinel + SessionStoreRouter resolve(`sessionStore.save(checkpoint)` in 4 turn 收口点)**;Spring 6 dual-ctor `@Autowired` 模式 Plan B #045 已实战验证(legacy 7-arg ctor 委托 8-arg ctor 传 `null` = NullSessionStore + primary 8-arg ctor `@Autowired` 显式标记);**e2e 必须验磁盘文件存在** —— `agent.session-store: file` 配置下完整 turn 后断言 `Files.list(sessionStoreDir).count() == 1` + JSON 内容含 `history.length > 0`,**不能只验 router.resolve()**(Story #014 IT 走 `@Autowired SessionStoreRouter` 反射验证,跑不到这个 gap);**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 #014 `LINGS-X01` SESSION_STORE_IO_FAILED)/ **0 行 dsh 改动**(等实施后 dsh v1.5.59 文档同步补 LinearTurnEngine 4 收口点 trySave 段 + §5.6.4 SPI 总表 Slot 5 SessionStore row 补 `Router stub 位置: §5.3.1.0` + §0 v1.5.58 → v1.5.59);**累计 Story 合入目标 47 个**(实施后);`specs/046-session-store-save-after-turn/spec.md` ~180 行(WHO/WHY/WHAT/AC 四段式,16 AC 跨 4 文件)+ `plan.md` ~280 行(6 阶段实施路径 + 5 决策点表 + 镜像 #045 precedent 对位表 + R-13 mitigation (d) baseline 镜像 pre-flight + demo-product 人工端到端 7 步清单)+ `tasks.md` ~380 行(24 steps 跨 5 phases + Phase 0 pre-flight + 16 AC ↔ task mapping + Story budget guards + R-13 第 30 次 PASS checklist)已落 2026-10-13;**修复者** Claude Code(根据用户 2026-10-13 会话反馈「现在就起 Story #046」,触发本 Story spec/plan/tasks 落地,等用户「实施吧」触发 Phase 1-5 实际代码改动);v1.3.54 Story #045 compactor-turn-wiring 文档同步 — dsh v1.5.57 → v1.5.58(§0 标题 v1.5.57 → v1.5.58 + §13 changelog 新增 v1.5.58 行 15 节 + §6.1 L3639-3643 LinearTurnEngine 段补 compactor invocation 4 步 `compactor.compact(history, toolResult)` + §5.6.4 SPI 总表 Slot 6 Compactor row 补 `Router stub 位置: §5.3.1.0`)+ **Slot 6 Compactor SPI 真接通 + 9 Slot SPI 体系 100% 真接通** —— Story #045 落地后 LingShu 9 个 Slot (1 PromptBuilder + 2 LlmProvider + 3 ToolExecutor + 4 PermissionPolicy + 5 SessionStore + 6 Compactor **本轮** + 7 MemorySource + 8 FlowEngine + 9 A2aTransport) 9/9 全部 100% 真接通(从 v1.5.28 §5.3.1.0 stub 阶段 → 真生产就绪,v0.1.0 GA 必要条件之一);`NullCompactor.java` sentinel (`Compactor` SPI 空实现,`compact(history, toolResult) → history` no-op) 保护 Story #001-#029 历史 fixture(没有 compactor 配置不调用任何 compression);`LinearTurnEngine` 7-arg ctor 新增 `Compactor compactor` final 参数 + `L186` compactor invocation 4 步触发(每 step ToolResult 后 / maxSteps 触发前 / cancel 前 / 异常前 4 段统一 `compactor.compact(history, lastToolResult)`)+ `AgentEvent.Compaction` 4 段 event 子类(`Started` / `Progress` / `Completed` / `Failed`)+ `TruncatingCompactor` 默认实现(`@Component` 由 `TruncatingCompactorProvider` 注册到 `CompactorRouter`,`priority=10` 胜过 `priority=0` 默认实现);`LinearTurnEngineProvider` 7-arg ctor 新增 `Routers.CompactorRouter compactorRouter` final 参数 + **Spring 6 dual-ctor @Autowired 模式**(legacy 6-arg ctor 委托 7-arg ctor 传 `null` = NullCompactor + primary 7-arg ctor 加 `@Autowired` 显式标记,Story #045 follow-up 必须显式 primary 标记否则 Spring 6 报 `BeanCreationException: no default constructor found`);`AgentFactory` 8-Router → 9-Router ctor(`@Autowired SessionStoreRouter` precedent 镜像,+ `CompactorRouter` 在 SlotResolver 不在 `AgentFactory` 直接 @Autowired,镜像 §5.3.1.0 第 8 隐式 Router concrete stub);`CompactorRouter` extends `SlotRouter<CompactorProvider, Compactor>` 第 8 隐式 Router concrete stub(v1.5.23 §5.3.1.0 stub 已写,本 Story 真接通);`CompactorRouter.resolve("truncating", cfg)` 名 resolution 接入 `agent.compactor: truncating` top-level camelCase YAML key(dsh §5.3.1.0 Slot 6 SPI);**12 new case** 跨 4 文件(`LinearTurnEngineCompactionTest` 7 L1 + `LinearTurnEngineProviderCompactionTest` 4 L1 + `AgentFactoryYamlCompactorIT` 1 L2 + `AgentFactoryYamlCompactorIT.yamlCustomCompactor_bindsTopLevel` 第 2 L2 case)+ 全 33 files 改动 + 106 blackbox tests pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree` diff 仅时间戳差异 = **0 binary delta 第 29 次 PASS**(纯 JDK 8 `Lombok @Value` + `Spring @Component` + `Autowired(required=false)` + `Nullable` 已锁 13 项依赖表内 0 新 binary 引入);`banned-dependencies` enforcer Rule 0: passed;**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 Story #044 现有 `LINGS-C02` 错误路径);**关键不变项** —— Compactor SPI 公开方法签名不变(`compact(List<Message>, ToolResult) → List<Message>` 不变)+ LinearTurnEngine 公开签名不变(只新增 1 个 ctor 参数 + AgentEvent.Compaction events 内部发射,不引入新公开方法)+ AgentFactory SPI 不变(8-Router ctor → 9-Router ctor 只 +1 Router 字段,公开方法签名 0 改动)+ AgentConfig 不可变契约不变(0 字段新增 —— `agent.compactor` 顶层 key 直接 binding 到 `cfg.getCompactor()` string)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系 100% 真接通 + JDK 8 兼容(`Lombok @Value` + `Spring @Component` + `Autowired(required=false)` + `Nullable` 已锁,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**累计 46 个 Story 合入**;**业务价值** —— **v0.1.0 GA 9 Slot 体系 100% 真接通里程碑达成**(LingShu Agent Engine 从 v1.5.28 stub 阶段 → 真生产就绪,**9 Slot = 9 Plugin (Provider) × Slot 全部可工作**,业务方按 dsh §5.6.4 SPI 总表 9 行 `agent.<slot>.name` 9 个 key 全部可工作不再 silent-ignored)+ **dsh §6 关键实现主链收口**(Story #001-#009e + #009 系列 A2A + #014 + #017 + #018-#045 = 完整 §6 主链 + 全部并行支链 100% ✅,dsh §6.1 / §6.2 / §6.3 / §6.4 / §6.5 全节对应 Story 已合)+ **Null Object pattern 保护历史 fixture**(`NullCompactor.INSTANCE` wire-in `Compactor` ctor 为 null 时,Story #001-#029 fixture 0 改动全过)/ **Spring 6 dual-ctor 实战验证 Plan B**(legacy 6-arg ctor + primary 7-arg ctor + `@Autowired` 显式标记,后续 #046+ 新增 SPI 时直接复用此模式)/ **Compactor 名 resolution 接入 dsh §5.3.1.0**(同 `PermissionPolicyRouter` / `ToolExecutorRouter` / `FlowEngineRouter` precedent,业务方 yml 写 `agent.compactor: <name>` 即可热切)+ **v0.1.0 GA 准备度 90% → 100%**(dsh §14 N1-N13 滞后项评估 + dsh §15 ErrorCode 复用 + constitution §10 R-13 缓解 Story 列表累计 **29 个 Story** 等);README.md「更新日期」段加 🆕 v1.5.58 Story #045 顶部 blockquote + 「核心特性」段补 🗜️ Slot 6 Compactor 真接通 + 9 Slot 100% bullet + 「Story 路线图」段追加 #045 retrospective(`NullCompactor` + `LinearTurnEngine` 7-arg ctor + `LinearTurnEngineProvider` 7-arg ctor + Spring 6 dual-ctor `@Autowired` 模式 + `AgentFactory` 9-Router ctor + `CompactorRouter` resolve + 12 case AC 黑盒验证 / 33 文件改动 / 全 106 tests pass / R-13 0 binary delta 第 29 次 PASS / 0 新 ErrorCode / 累计 46 个 Story)+ specs/ROADMAP.md 段一 ✅ 已完成加 #045 行(2026-10-10,106 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 29 次 PASS / 0 新 ErrorCode)+ 段二 🟡 待补 #045 划掉(§6 关键实现主链 + 全部并行支链 100% ✅ 🎉 庆祝!)+ 段五 🎯 实施节奏 累计计数 45 → **46 个 Story** + `constitution.md` §10 R-13 缓解 Story 列表补 `#045` 行**第 29 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 28 → **29 个 Story** + dsh §13 v1.5.58 行新增 15 节记录本 Story 完成 + dsh §6.1 L3639-3643 LinearTurnEngine 段补 compactor invocation 4 步 `🆕 v1.5.58 Story #045` marker + §5.6.4 SPI 总表 Slot 6 Compactor row「Router stub 位置」字段补 +「对应设计文档」版本号 v1.5.57 → v1.5.58;**修复者** Claude Code(根据用户 2026-10-10 会话反馈「Story #045 Slot 6 Compactor 真接通 9 Slot 100% 真接通 落地」+「实施吧」,触发本 Story 实施 + 12 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 29 次 PASS 0 binary delta**);v1.3.53 Story #014 session-store-multi-backend 文档同步 — **dsh §6 关键实现主链 Slot 5 `SessionStore` SPI 真接通** —— `Checkpoint.@Value` 加 `@JsonCreator` + `@JsonProperty` 4-arg ctor(Lombok `@Value` 自动 final class 缺 default ctor,Jackson round-trip 必须)+ `Message.@JsonTypeInfo` + `@JsonSubTypes` 4 子类多态 round-trip(`Checkpoint.history: List<Message>` 序列化/反序列化)+ `FileSessionStore` concrete class(`@Component` 移除,plain class;`save/load/list/delete` 4 方法契约;Atomic file write 协议 `Files.write(.tmp) → Files.move(.tmp → .json, ATOMIC_MOVE, REPLACE_EXISTING)` + `AtomicMoveNotSupportedException` fallback `StandardCopyOption.REPLACE_EXISTING`;multi-tenant `resolveBaseDir()` 走 `TenantContext.current()` + `_default` sentinel;`validateSessionId()` 正则 `[a-zA-Z0-9._-]{1,128}` 防 path traversal;`IOException` + `JsonProcessingException` 全部 wrap 进 `LingsSessionStoreException[LINGS-X01]`)+ `FileSessionStoreProvider` SPI(`name="file"` + `priority=10` + `version="1.0.0"`,`@Component` 移除)+ `InMemorySessionStoreProvider` SPI + `DefaultInMemorySessionStore` 删 `@Component`(plain class + 工厂模式)+ `SessionStoreAutoConfiguration`(`@Configuration` + 2 `@Bean` = `memorySessionStoreProvider()` + `fileSessionStoreProvider()`,均 `@Bean(name="sessionStoreProvider_<name>-1.0.0")` v1.5.28 §5.5 多 Provider 模式样板)+ `Routers.SessionStoreRouter`(extends `SlotRouter<SessionStoreProvider, SessionStore>`,super 传 `"SessionStore"` + Logger,Slot 5 第 9 个 Router,**第 9 隐式 Router** —— 按 dsh §5.3.1.0 §5.3.1.1 模式,**不**在 SlotResolver,直接 `@Autowired` 进 `AgentFactory` ctor 与 `RuntimeSandboxRouter` / `FlowEngineRouter` 并列;`RuntimeSandboxRouter` precedent 镜像 — Story #028 同模式落地过)+ `AgentFactory` ctor 加 `@Autowired SessionStoreRouter`(从 7-Router → 8-Router,**只新增 1 个 Router 字段**)+ `LingsSessionStoreException`(`RuntimeException` 子类,`getMessage()` 自动前缀 `[LINGS-X01]`)+ `SessionStoreErrorCodes.LINGS_X01 = "LINGS-X01"` 常量类(X 域 1 号 = `SESSION_STORE_IO_FAILED`,**第 12 域字母 `X = SessionStore` 启用**,自 #023 后第二次启用新 ErrorCode 域);**13 new case** 跨 4 文件(`FileSessionStoreTest` 5 L1 save/load/validate/atomic + `InMemorySessionStoreProviderTest` 4 L1 SPI metadata + create + check + `SessionStoreAutoConfigurationTest` 4 L2 `@Configuration` + 2 `@Bean` + 2 distinct Bean 名 + `create()` 返回正确 concrete + `SessionStoreRouterIT` 5 L2 Router.byName Map size=2 + multi-Provider 模式 + resolve 各返 + 2 Provider 无 `@Component`);`mvn -pl lingshu-core dependency:tree` pre/post diff 仅时间戳差异 = 0 binary delta(`Files.move` + `AtomicMoveNotSupportedException` + `StandardCopyOption` + Jackson `@JsonTypeInfo` + `@JsonSubTypes` + `@JsonCreator` + `@JsonProperty` + Spring `@Configuration` + `@Bean` + Lombok `@Value` 已锁 13 项依赖表内 0 新 binary 引入);`banned-dependencies` enforcer Rule 0 passed;**关键不变项** —— `SessionStore` SPI 不变(只新增 2 真实现 + 1 Provider 双后端 + 1 AutoConfiguration)+ `SessionStoreProvider` SPI 不变(`name()` / `priority()` / `version()` / `create(AgentConfig)` 公开方法签名 0 改动,只 `InMemorySessionStoreProvider` 删 `@Component` 注解 1 处)+ `SessionStoreRouter` 行为按 `name()` 路由不变(`agent.session-store.name: file` 走 `FileSessionStore` / `agent.session-store.name: memory` 走 `DefaultInMemorySessionStore`,yml 配置 back-compat)+ `SessionStoreAutoConfiguration` SPI 不变(`@Configuration` + 2 `@Bean` Bean 名 `sessionStoreProvider_memory-1.0.0` + `sessionStoreProvider_file-1.0.0` 互不冲突)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `AgentFactory` SPI 不变(7-Router ctor → 8-Router ctor,**只新增 1 个 Router 字段**)+ `Message` 4 子类契约不变(只 Jackson 多态注解新增,字段 shape 0 改动)+ `Checkpoint.@Value` 4-arg ctor 不变(只 Jackson 注解新增)+ `Tool` SPI 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `LinearTurnEngine` 公开签名不变 + §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变(仍 9 Slot,`SessionStore` 是 Slot 5,之前 dsh §5.3.1.0 stub 已写但未真接通,本 Story 真接通)+ JDK 8 兼容(`Files.move` + `AtomicMoveNotSupportedException` + Jackson 已锁,no `var` / `List.of` / sealed / records);**累计 45 个 Story 合入**(主链 + 全部并行支链 100% ✅ + MCP + A2A 沙箱防护深度双闭合 + PermissionPolicy 3rd outcome + ApprovalRegistry SSE round-trip + 3 Provider 注册路径对齐 v1.5.28 + ApprovalGate SPI 真接通 + 2 latent bug + 2 pre-existing bug 修复 + demo-product ask-mode 前端 UI 补全 + AnthropicLlmProvider I/O 池有界化 hygiene fix + AgentConfig 顶层 turn concurrency cap 顶层闸门 + SessionStore memory + file 双后端);**修复者** Claude Code(根据用户 2026-10-08 会话反馈「Story #014 SessionStore 多后端把 file + memory 落了」+「实施吧」,触发本 Story 实施 + 13 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 28 次 PASS 0 binary delta**);v1.3.52 Story #044 agent-turn-concurrency-cap 文档同步 — dsh v1.5.56 → v1.5.57(§0 标题 v1.5.56 → v1.5.57 + §13 changelog 新增 v1.5.57 行 19 节 + §10 NFR 表「并发 turn 数」行 footnote 兑现 marker ✅ Story #044 已落地)+ **AgentConfig 顶层扩 2 字段 + 顶层 validate()** —— `AgentConfig.@Value` 24 → 26 字段 final,顶层扩 `maxConcurrentTurns`(默认 16)+ `maxConcurrentQueueDepth`(默认 32)2 int 字段,空 yml 自动 fallback 16 + 32,top-level camelCase YAML keys `agent.maxConcurrentTurns` / `agent.maxConcurrentQueueDepth` 自动 binding;`AgentConfig` 类底部新增顶层 `validate()` 方法(对齐 Story #018 `CompactorConfig.validate()` precedent),聚合 `reactMaxSteps` + `maxConcurrentTurns` + `maxConcurrentQueueDepth` 3 字段检查到单 `LingsConfigException`,复用现有 `LINGS-C02` 错误路径;**3 配置文件同步** —— `AgentConfigDefaults` 26 字段 final ctor 末尾追加 `16, // 🆕 Story #044 ... maxConcurrentTurns` + `32, // 🆕 Story #044 ... maxConcurrentQueueDepth` 实参 + `AgentFactory.toAgentConfig` 末尾追加 `intOr(agent, "maxConcurrentTurns", 16)` + `intOr(agent, "maxConcurrentQueueDepth", 32)` + `new AgentConfig(...)` 末尾追加 2 实参;**53 fixture 文件 mechanical sync** —— Lombok `@AllArgsConstructor` 24 → 26 参数签名 auto-regen,53 fixture 文件(`AgentConfig.builder().flowEngine("...").llm(Llm.builder()...)...permissionPolicy("...")`)末尾机械追加 `.maxConcurrentTurns(16).maxConcurrentQueueDepth(32)` 2 字段,**所有 fixture 0 业务逻辑改动只参数补齐**,R-13 mitigation (d) baseline 镜像 `bb6627104920b580322931b7b33a0806` pre/post md5sum 相同 = **0 binary delta 第 27 次 PASS**;**13 new case** 跨 2 文件(`AgentConfigConcurrencyCapValidationTest` 8 L1:`defaults_passValidation` / `positiveCustomValues_passValidation` / `zeroMaxConcurrentTurns_throwsLingsC02` / `negativeMaxConcurrentTurns_throwsLingsC02` / `zeroMaxConcurrentQueueDepth_throwsLingsC02` / `negativeMaxConcurrentQueueDepth_throwsLingsC02` / `allFieldsZero_aggregatesAllErrors` / `AgentConfigDefaults_passValidation`+ `AgentFactoryYamlConcurrencyCapIT` 5 L2:`yamlDefault_parses16And32` / `yamlCustomMaxTurns_parsesCustom` / `yamlCustomQueueDepth_parsesCustom` / `yamlBothCustom_parsesBoth` / `yamlZeroMaxTurns_throwsC02OnValidate`);720 全 lingshu-core 测试 0 regression(707 旧 0 改动 + 13 新 case);**0 新 ErrorCode**(复用现有 `LINGS-C02` Config 域 C 段 2 号路径)/ **0 新 Maven 依赖** / 累计 44 个 Story 合入;**关键不变项** —— `AgentConfig` 不可变契约不变(`@Value` + `@AllArgsConstructor` Lombok 自动 final 注入 26 字段 final,业务三件套 Identity/Instructions/Memory 0 改动)+ `LlmProvider` SPI 不变 + `Message` 4 子类契约不变 + `Tool` SPI 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `LinearTurnEngine` 公开签名不变 + `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + JDK 8 兼容(纯 `@Value` int 字段 + `List<String>` 聚合 + `String.format` + Lombok `@AllArgsConstructor` 自动 re-regen,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**业务价值** —— **dsh §10 NFR row 4「默认 16,排队 ≤ 32」数字基线 docs/code 落地**(原文仅 NFR 表一行,Story #044 落地后 NFR 表 + AgentConfig 字段 + yml 接线 3 处对齐,业务方按 dsh 配 `agent.maxConcurrentTurns: 16` 现在真 binding 到顶层字段不再 silent-ignored)/ **Story #043 forward reference 兑现**(Story #043 §10 NFR row 4 footnote 写「Story #044 处理 docs/code 兑现」,本 Story 兑现)/ **dsh/code drift 收口**(24 → 26 字段 AgentConfig schema 覆盖 dsh §10 NFR row 4 + Story #043 配套 + Story #044 顶层并发闸门)/ **顶层 validate() 守护**(3 字段聚合错误信息,启动期 fail-fast,避免运行时 N-concurrent-turn × bounded-pool 误配导致 OOM 风险)/ **Future-proof**(未来加并发 turn scheduling 时 N 并发 turn × 单池上限 = 仍有界,Story #043 单池已有界 + Story #044 顶层闸门 = 双重闸门);**修复者** Claude Code(根据用户 2026-10-06 会话反馈「Story #044只创建了一个spec目录,这个也继续吧」+「实施吧」,触发本 Story spec/plan/tasks 落地 + 13 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 27 次 PASS 0 binary delta**);**下一步走** §14 N6 graceful-shutdown(LINGS-L03 reserved 启用)或 §14 N10 audit-log 或 §14 N4 CostBudget 滞后项;constitution §10 R-13 缓解 Story 列表补 `#044` 行**第 27 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 27 → **28 个 Story**;dsh §13 新增 v1.5.57 行(后者 19 节记录本 Story 完成)+ dsh §10 NFR row 4 footnote 兑现 marker(原 Story #043 forward ref 兑现);README.md 「更新日期」段加 🆕 v1.5.57 Story #044 顶部 blockquote + 「核心特性」段补 🛡️ AgentConfig 顶层 turn 并发闸门 bullet + 「Story 路线图」段追加 #044 retrospective;specs/ROADMAP.md 段一 ✅ 已完成加 #044 行 + 段五 🎯 实施节奏 累计计数 43 → **44 个 Story**;v1.3.51(v1.3.51 Story #043 anthropic-llm-io-bounded-pool 文档同步 — dsh v1.5.55 → v1.5.56(§0 标题 v1.5.55 → v1.5.56 + §13 changelog 新增 v1.5.56 行 19 节 + §10 NFR 表「并发 turn 数」行加 footnote → Story #044 处理 docs/code 兑现)+ **AnthropicLlmProvider.ioExecutor 镜像 ToolExecutorConfig.agentToolPool 有界池 hygiene fix** —— 项目里 `lingshu-core/src/main/java/` 唯一一处无界池生产代码 `Executors.newCachedThreadPool(maximumPoolSize=Integer.MAX_VALUE)` 替换为有界 `ThreadPoolExecutor`(`corePoolSize=Runtime.getRuntime().availableProcessors()*2` + `maxPoolSize=corePool*2` + `keepAliveTime=60s` + `queue=LinkedBlockingQueue(256)` + `RejectedExecutionHandler=CallerRunsPolicy` + 线程名前缀 `anthropic-llm-io-N`(per-instance `AtomicInteger` 计数,镜像 `lingshu-tool-N` 风格)+ `daemon=true` 守住 JVM exit 不阻塞);**镜像 `ToolExecutorConfig.agentToolPool` 100% shape 对齐表** —— `corePoolSize` / `maxPoolSize` / `keepAliveSeconds` / `queueCapacity` / `Queue 类型` / `RejectedHandler` / `daemon` 7 字段全等,唯一差异 = 线程名前缀(`lingshu-tool-N` vs `anthropic-llm-io-N`)+ per-instance 计数;**`CallerRunsPolicy` 在队列满时让 `LinearTurnEngine` 主线程兜底执行 —— back-pressure 慢不丢**(I/O 阻塞堆叠 → 主线程承担 → LLM SSE 反压 → 自然反压到 Agent runBlocking 调用方,根除无界 `Integer.MAX_VALUE` 线程数爆炸 OOM 风险);**2 文件改动** —— `lingshu-core/src/main/java/ai/lingshu/core/impl/llm/AnthropicLlmProvider.java` modify(5 import 替换 `Executors` 删 / `LinkedBlockingQueue` / `ThreadFactory` / `ThreadPoolExecutor` / `TimeUnit` / `AtomicInteger` 新增 + `ioExecutor` 字段替换 `Executors.newCachedThreadPool(r -> ...)` → `buildBoundedIoExecutor()` + 类级 Javadoc +78 行净增)+ `lingshu-core/src/test/java/ai/lingshu/core/impl/llm/AnthropicLlmProviderBoundedPoolTest.java` new(7 case L1 unit 反射型验证 `ioExecutor` 字段 + ThreadPoolExecutor shape + LinkedBlockingQueue(256) + CallerRunsPolicy + 线程名前缀 + daemon);**7 new case L1**(`poolType_isThreadPoolExecutor` 反射 `getClass() == ThreadPoolExecutor.class` 不是子类包装 / `corePoolSize_isAvailableProcessorsTimes2` / `maxPoolSize_isCoreTimes2` / `keepAliveTime_is60Seconds` / `queue_isLinkedBlockingQueueOf256` `remainingCapacity() + size() == 256` / `rejectedHandler_isCallerRunsPolicy` / `threadName_prefix_isAnthropicLlmIo_N_daemon` submit Runnable + `Thread.getAllStackTraces()` 找 `anthropic-llm-io-N` + `t.isDaemon() == true`);**707 pass / 0 fail / 2 MCP heartbeat flake pre-existing**(与本 Story 无关);**反向 AC 验证**(RAC-1—RAC-3)—— 既有 `AnthropicLlmProviderTest` 13 case(Story #027a + #027b)+ `AnthropicStreamProviderIT` 2 case + `AnthropicToolReActIT` 2 case + `AnthropicStreamEventTest` 2 case + `AnthropicStreamParserTest` 6 case + `LingsLlmProviderExceptionTest` 1 case **0 改动全过**;full suite 700 旧 case 0 改动全过(707 - 7 = 700);`mvn -pl lingshu-core dependency:tree` pre/post md5sum 相同(`9b7a46af5f08013ea85a7e9ca7d89c2e`,仅时间戳差异)= **0 binary delta 第 26 次 PASS**(纯 JDK 8 内置 `ThreadPoolExecutor` + `LinkedBlockingQueue` + `AtomicInteger` + `ThreadFactory` + `CallerRunsPolicy` 0 新 binary 引入);`banned-dependencies` enforcer Rule 0: passed;**关键不变项** —— `LlmProvider` SPI 不变(公开方法签名 `stream(Prompt, TurnContext, Subscriber<AgentEvent>) → CompletableFuture<LlmResponse>` 不变,只换底层实现,字段类型仍是 `ExecutorService`)/ `AnthropicLlmProvider` 6-arg ctor 不变 + `doPostStream` 私有方法签名不变 / `Message` 4 子类契约不变(🆕 v1.5.46 refactor 已落)/ `Prompt` + `ToolSpec` + `ToolRegistry.modelVisibleSpecs()` 不变 / `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)/ `Tool` SPI 不变 + `LinearTurnEngine` 公开签名不变 / `AgentConfig` 不可变契约不变(0 字段新增 —— Story #044 才加 `maxConcurrentTurns`)/ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)/ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + 24 字段 AgentConfig schema 不变 + JDK 8 兼容(`ThreadPoolExecutor` + `LinkedBlockingQueue` + `AtomicInteger` + `ThreadFactory` + `CallerRunsPolicy` 全 JDK 8 内置,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住);**0 新 Maven 依赖** / **0 新 ErrorCode** / R-13 mitigation (d) baseline 镜像 **第 26 次 PASS 0 binary delta**;**业务价值** —— **根除项目里 `lingshu-core/src/main/java/` 唯一一处无界池生产代码**(生产环境若 LLM 上游慢 + Anthropic SDK 抖动 + Agent runBlocking 高并发 → 无界 `Integer.MAX_VALUE` 线程数爆炸 OOM,本 Story 用有界 `ThreadPoolExecutor` + `CallerRunsPolicy` 兜底,back-pressure 传导主线程 → 业务方自然限流)/ **mirror `ToolExecutorConfig.agentToolPool` 100% shape** —— 项目有界池模板统一,后续 plugin 贡献者 / 内部新增组件参考 `agentToolPool` 样板即可 / **0 行为变化**(单 turn 1 任务在飞现状下有界 vs 无界行为等价,queue 永不满,CallerRunsPolicy 永不被触发 —— 现有 38 case Anthropic regression suite 0 改动全过为证)/ **hygiene-only fix**(R-13 mitigation (d) baseline 镜像 pre/post md5sum 相同 = 第 26 次 0 binary delta PASS,累计 26 个 R-13 (d) PASS 故事无新增 binary);**修复者** Claude Code(根据用户 2026-10-06 会话反馈「AnthropicLlmProvider.ioExecutor 是项目里唯一一处无界池,根除一下 + 镜像 ToolExecutorConfig 有界池 shape」+「实施吧」,触发本 Story 实施 + 7 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 26 次 PASS 0 binary delta**);README.md 「更新日期」段加 🆕 v1.5.56 Story #043 顶部 blockquote + 「核心特性」段补 🧵 bounded I/O pool bullet + 「Story 路线图」段追加 #043 retrospective(镜像 `ToolExecutorConfig.agentToolPool` 100% shape 有界 ThreadPoolExecutor + CallerRunsPolicy 兜底 + 2 文件改动 + 7 case L1 + R-13 0 binary delta 第 26 次 PASS / 0 新 ErrorCode / 0 新 Maven 依赖 / 累计 43 个 Story 合入)+ specs/ROADMAP.md 段一 ✅ 已完成加 #043 行(2026-10-06,707 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 26 次 PASS / 0 新 ErrorCode)+ 段二 🟡 待补 #043 不适用(本 Story 不在 §6 主链 §6.3 / §6.5 提议列表中,纯 hygiene)+ 段五 🎯 实施节奏新增第 22 项 #043 retrospective + 累计 42 → **43 个 Story** + `constitution.md` §10 R-13 缓解 Story 列表补 `#043` 行**第 26 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 27 → **28 个 Story** + dsh §13 v1.5.56 行新增 19 节记录本 Story 完成 +「对应设计文档」版本号 v1.5.55 → v1.5.56;v1.3.50(v1.3.50 Story #042 demo-product-frontend-approval-ui 文档同步 — dsh v1.5.54 → v1.5.55(§0 标题 v1.5.54 → v1.5.55 + §13 changelog 新增 v1.5.55 行 18 节)+ **Story #030 + #041 后端 ApprovalGate + demo-product ChatController SSE round-trip 落地 + ApprovalRegistry 终局收集器** —— demo-product frontend 补齐 Allow/Deny button UI 闭合成完整 demo loop — **2 文件改动 +50 行纯 vanilla HTML/CSS/JS**(zero npm 依赖,zero framework)+ `index.html` 25 行 CSS-only 增补(`.reason` 输入框 200px + `.btn-allow` 绿色 + `.btn-deny` 红色 + `:disabled` 灰态 + `.approval-err` 红字错误提示)+ `app.js` 25 行 vanilla JS 增补(`else if (eventName === 'approval')` 分支调 `postDecision()` 帮手函数,`POST /api/approvals/{sessionId}/{approvalId}` body `{decision, reason}` + `fetch()` SSE round-trip + `btn.disabled = true` 防重复提交 + `approval-err` span 显示错误信息 + try/catch/finally 完整异常路径)+ AgentEventMapper 已落 Story #030 把 ApprovalRequired 子类映射为 `{type:"approval", approvalId, ask}`,本 Story 只补 frontend handler **不动**后端任何 Java 代码 + 后端契约 `POST /api/approvals/{sessionId}/{approvalId}` body `{decision:"allow"|"deny", reason:"..."}` 在 Story #030 已落 + demo-product 端到端 demo loop 完整闭环(Agent 触发危险 tool → SSE 推 ApprovalRequired → 前端 button 显示 + 用户点 allow/deny → POST 回 /api/approvals → 后端 ApprovalRegistry.complete(decision) → LinearTurnEngine 解阻塞 + ToolExecutor 继续 → 最终回复 user)+ **人工端到端 7 步清单**(README.md 同步更新) —— (1) `mvn -pl lingshu-examples/demo-product spring-boot:run`(2) curl `http://localhost:8080/` 确认 HTML 加载(3) 配 `agent.permission-policy: ask` + `tools.ask-list: [bash]`(4) 输入 "list current directory" 触发(5) SSE events 区看 ApprovalRequired card + Allow/Deny 按钮(6) 点 Allow → ToolExecutor 继续 + 最终回复(7) 再试一次点 Deny → Deny reason 进 SSE + 拒绝 ToolResult 回主 Agent 路径;**0 new case**(前端改动无 JUnit 覆盖,人工端到端 7 步清单替代 AC 黑盒)+ **0 后端 Java 改动**(0 Java 编译 0 regression)+ **0 新 Maven 依赖**(zero npm 依赖,zero framework,纯 JDK 8 内置 `fetch` + `URLSearchParams` + `EventSource` 浏览器内置)+ **0 新 ErrorCode**(复用 #030 LINGS-P02 + Story #030 后端契约不变)/ R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-examples/demo-product dependency:tree` diff **仅时间戳差异** = **0 binary delta 第 25 次 PASS**(前端 .html + .js 静态资源不进 dep-tree,baseline 镜像 0 Java 改动 0 binary 引入);**关键不变项** —— `ApprovalGate` SPI 接口契约不变(只 frontend UI 补全 不动后端任何 SPI)+ `Decision` 3 子类(Allow / Deny / AskUser)不变 + `AgentEvent.ApprovalRequired` 3-arg ctor 不变(Story #030 已落)+ `ApprovalRegistry` @Component 不变(Story #030 已落 `ConcurrentMap`)+ `AskUserPermissionPolicy` 行为不变(Story #030 已落 5 段决策)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ `ChatController.POST /api/approvals/{sessionId}/{approvalId}` 端点不变(Story #030 已落)+ `AgentEventMapper` 不变(Story #030 已落 `{type:"approval", approvalId, ask}` 映射)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 9 Slot 体系不变 / 24 字段 AgentConfig schema 不变 + JDK 8 兼容(`fetch` + `URLSearchParams` + `EventSource` 浏览器内置 + vanilla JS 0 新 binary 引入,no `var` / `List.of` / sealed / records);**累计 42 个 Story 合入**;**业务价值** —— **demo-product 端到端 demo loop 完整闭环**(Story #030 后端 ApprovalGate SPI + Story #041 DefaultApprovalGate 抽 SPI + 本 Story frontend button UI = 完整 round-trip,**用户在 demo-product 浏览器实测 allow/deny** 验证 ask-mode privilege escalation 防护真实生效,文档示例可截屏)+ **前端改动最小化**(2 文件 +50 行,zero npm 依赖,zero framework,沿用 demo-product vanilla JS 既有架构)+ **Story #030 + #041 + #042 三件套完成 ask-mode 完整实现链路**(Story #030 = AskUser 3rd outcome 真接通 / Story #041 = ApprovalGate SPI 抽离 + 2 latent bug 修复 + 2 pre-existing 编译错误修复 / Story #042 = frontend UI 闭环)+ **R-04 privilege escalation 缓解完整链路**(分值 8 —— Story #029 strict policy + Story #030 ask policy + Story #031 pattern 通配 + Story #041 SPI 抽离 + Story #042 frontend UI = 完整可工作演示)+ **demo-product 作为 ask-mode 旗舰示例**(面向用户作为可工作的 demo,文档/博客/演示视频可截屏);README.md 「更新日期」段加 🆕 v1.5.55 Story #042 顶部 blockquote + 「核心特性」段补 🖱️ demo-product frontend approval UI bullet + 「Story 路线图」段追加 #042 retrospective(Story #030 + #041 后端落地 + frontend 补全 + 2 文件 +50 行 vanilla JS + 人工端到端 7 步清单 / R-13 0 binary delta 第 25 次 PASS / 0 新 ErrorCode / 0 后端 Java 改动 / 累计 42 个 Story)+ specs/ROADMAP.md 段一 ✅ 已完成加 #042 行(2026-10-04,no test runner / R-13 0 binary delta 第 25 次 PASS / 0 新 ErrorCode)+ 段二 🟡 待补 #042 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 42 已合 / 0 待补 / +0 新 Java case(前端改动 0 JUnit 覆盖,人工端到端 7 步清单替代)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#042` 行**第 25 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 24 → **25 个 Story** + dsh §13 v1.5.55 行新增 18 节记录本 Story 完成 +「对应设计文档」版本号 v1.5.54 → v1.5.55;**修复者** Claude Code(根据用户 2026-10-03 会话反馈「Story #030 + Story #041 后端 ApprovalGate 已落地,但 demo-product 前端 SSE event stream UI 没补 Allow/Deny 按钮,用户在前端看到的只是 approval 文本无交互能力,补一下让 demo 闭环」+「实施吧」,触发本 Story 实施 + 2 文件 +50 行 + R-13 mitigation (d) baseline 镜像 **第 25 次 PASS 0 binary delta**);v1.3.49(v1.3.49 Story #041 approval-gate-wiring-cleanup 文档同步 — dsh v1.5.53 → v1.5.54(§0 标题 v1.5.53 → v1.5.54 + §13 changelog 新增 v1.5.54 行 17 节)+ **Story #030 ask-mode wiring 抽 SPI 实战验证 Plan B** —— `ApprovalGate` SPI interface 在 `ToolExecutionContext` 嵌套接口留位(`Decision ask(Decision.AskUser) → Decision` 单方法 + Javadoc「阻塞-解阻塞回合」) + `DefaultApprovalGate` private static final inner class on `DefaultToolExecutionContext`(复用 Story #030 inline `LinearTurnEngine.dispatchWithPolicy` 4 段逻辑 + `try/finally { registry.unregister(approvalId); }` 清理 ApprovalRegistry 防内存泄漏 + scenario A 正常 allow + scenario B 正常 deny + scenario C timeout → `[LINGS-P02]` + scenario D error → future.completeExceptionally + sink 收 `[LINGS-P02]`)+ `LinearTurnEngine.dispatchWithPolicy` AskUser branch refactor(inline 6 段 → 4 段 + 委托 `DefaultApprovalGate.ask()`)+ **2 latent bug 修复** —— cancel callback 重复注册(`AtomicBoolean fired` + `compareAndSet(false, true)` 守卫 callback 重复触发)+ explicitSink mismatch(`execute()` 直接 `throw RuntimeException` 跳 approve gate → null continuation 抛 NPE;修后显式 sink 收 `ToolResult.error("[LINGS-P02] Approval timed out: ...")` 让 LinearTurnEngine 正常收口)+ **2 pre-existing 编译错误修复** —— `DemoProductA2aServerApplication` 类 import 引用 default 包 → 修后 import 正确 + `CliRunner` ctor 引用 `CliSkillToolExecutionContext` private 静态内部类反射构造器缺失 no-arg 路径 → 修后 `SkillCommandDispatcher.handleUserInput` 走 default 公开 ctor;**9 new case** 跨 5 文件(`DefaultApprovalGateTest` 5 L1:happy allow + happy deny + timeout → `[LINGS-P02]` + error → future.completeExceptionally + registry-cleanup-on-finally + `A2aServerToolExecutionContextApprovalTest` 3 L2 fail-safe stub deny + `SkillCommandDispatcherStubApprovalTest` 3 L2 fail-safe stub deny + `DemoProductA2aServerApprovalTest` 1 L2 fail-safe stub deny + `DemoProductAskUserSpiWiringIT` 1 L3 demo-product wiring round-trip,用 `@SpringJUnitConfig` + `@Import(ApprovalRegistry.class)` focused context 替代全 `@SpringBootTest`);**691 pass / 0 fail / 2 MCP heartbeat flake pre-existing**(与本变更**无关**,CLAUDE.md 已记录);R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core,lingshu-cli,lingshu-a2a-server,lingshu-examples/demo-product,lingshu-examples/demo-product-a2a-server dependency:tree` diff **仅时间戳差异** = **0 binary delta 第 24 次 PASS**(纯 Java refactor + `AtomicBoolean.compareAndSet` + JDK 8 `try/finally` + Lombok `@Value` + Spring `@Component` + `ConcurrentHashMap` + `ConcurrentMap` + `CompletableFuture` + `UUID.randomUUID` + Jackson `@JsonProperty` kebab-case 已锁 13 项依赖表内 0 新 binary 引入);**关键不变项** —— `ApprovalGate` SPI 接口契约不变(只 new SPI 公开接口位置留出)+ `Decision` 3 子类(Allow / Deny / AskUser)不变 + `AgentEvent.ApprovalRequired` 3-arg ctor 不变(Story #030 已落)+ `ApprovalRegistry` @Component 不变(Story #030 已落 `ConcurrentMap`)+ `AskUserPermissionPolicy` 行为不变(Story #030 已落 5 段决策)+ `DefaultToolExecutionContext` 公开方法签名不变(只新增 private static final inner class `DefaultApprovalGate`)+ `LinearTurnEngine` 公开方法签名不变(只 `dispatchWithPolicy` AskUser branch 内部 refactor)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + JDK 8 兼容(`AtomicBoolean.compareAndSet` + JDK 8 `try/finally` + `ConcurrentHashMap` + `CompletableFuture` + `UUID.randomUUID` + `Collections.emptyList()` + `Arrays.asList()` + Lombok `@Value` + Spring `@Component` + Jackson `@JsonProperty` kebab-case 已锁,no `var` / `List.of` / sealed / records);Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**累计 41 个 Story 合入**;**业务价值** —— **Story #030 ask-mode wiring 抽 SPI 实战验证 Plan B**(inline logic → `ApprovalGate` + `DefaultApprovalGate` 是技术债清理的最佳路径,fix 2 latent bug + 2 pre-existing 编译错误的同时把 inline logic 抽为可单测 SPI;测试 dry-run 反射 verify `DefaultApprovalGate` 行为而不需要 mock full ApprovalRegistry)/ **2 latent bug 修复**(cancel callback 重复注册 + explicitSink mismatch,这些 bug 在 Story #030 inline 路径下会被偶发触发,Story #041 抽 SPI 时一次性 fix)/ **2 pre-existing 编译错误修复**(demo-product-a2a-server `Application` import + CLI ctor 反射构造器,Story #041 抽 SPI 时补全 no-arg ctor 路径)/ **`ApprovalRegistry` 内存泄漏防护**(Story #030 inline 路径下异常分支可能泄漏 registry entries,Story #041 `DefaultApprovalGate.try/finally { registry.unregister(approvalId); }` 保证不管 any path 都清理)/ **`A2aServerToolExecutionContext` / `SkillCommandDispatcher.CliSkillToolExecutionContext` / `DemoA2aServer.StubToolExecutionContext` fail-safe stub 加 AC marker 守住不退步**;README.md 「更新日期」段加 🆕 v1.5.54 Story #041 顶部 blockquote + 「核心特性」段补 🆕 ApprovalGate SPI 抽离与 2 latent bug 修复 bullet + 「Story 路线图」段追加 #041 retrospective(`ApprovalGate` SPI 抽离 + `DefaultApprovalGate` private static final inner class + 2 latent bug 修复 + 2 pre-existing 编译错误修复 + `LinearTurnEngine.dispatchWithPolicy` AskUser branch refactor + 9 case AC 黑盒验证 / 12 existing fixture 0 改动 / R-13 0 binary delta 第 24 次 PASS / 0 新 ErrorCode / 累计 41 个 Story)+ specs/ROADMAP.md 段一 ✅ 已完成加 #041 行(2026-10-03,691 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 24 次 PASS / 0 新 ErrorCode)+ 段二 🟡 待补 #041 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 41 已合 / 0 待补 / +9 新 case(= 691 = 679 pre-#041 chain + 12 新 net new case)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#041` 行**第 24 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 23 → **24 个 Story** + dsh §13 v1.5.54 行新增 17 节记录本 Story 完成 +「对应设计文档」版本号 v1.5.53 → v1.5.54;**修复者** Claude Code(根据用户 2026-10-02 会话反馈「Story #030 ask-mode wiring 抽 SPI」+ 用户 2026-10-03 实施反馈「继续」,触发本 Story 实施 + 9 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 24 次 PASS 0 binary delta**);v1.3.48(v1.3.48 Story #037 permission-policy-multi-provider-alignment 文档同步 — dsh v1.5.52 → v1.5.53(§0 标题 v1.5.52 → v1.5.53 + §13 changelog 新增 v1.5.53 行 + §5.5 末补 Story #037 marker)+ **Slot 4 PermissionPolicy 3 Provider 注册路径 100% 对齐 v1.5.28 §5.5 多 Provider 模式** —— `AllowAllPermissionPolicyProvider` 删 `@Component` + 删 `import org.springframework.stereotype.Component`(Story #001 残留的 `@Component` 模式与 v1.5.28 §5.5 多 Provider 模式不符,实测发现 2026-10-02 实施 Story #030 demo-product 测试时)+ `PermissionPolicyAutoConfiguration` 加 `@Bean(name="permissionPolicyProvider_default-1.0.0") public PermissionPolicyProvider defaultPermissionPolicyProvider()` 平行 strict / ask 两个 `@Bean`,3 Provider 命名风格 `permissionPolicyProvider_<name>-<version>` 100% 一致;9 new case 跨 2 文件(`AllowAllPermissionPolicyProviderTest` 4 L1 + `PermissionPolicyRouterMultiProviderIT` 5 L2,含 Router 内部 byName Map size=3 + US2-AS5 `AllowAllPermissionPolicyProvider` 无 `@Component` 注解反射验证);**688 pass / 0 fail / 2 MCP heartbeat flake pre-existing** / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core,lingshu-examples/demo-product dependency:tree` diff **仅时间戳差异** = **0 binary delta 第 23 次 PASS**(纯 Java 文件内 `@Component` 删除 + `@Bean` 注册 + JDK 8 反射 `getDeclaredField` / `getDeclaredAnnotation` / `setAccessible` 已锁 0 新 binary 引入);**关键不变项** —— `PermissionPolicy` SPI 不变(公开方法签名 `check(Tool, ToolExecutionContext) → Decision` 不变,只迁注册路径)+ `Decision` 3 子类(Allow / Deny / AskUser)不变 + `PermissionPolicyProvider` SPI 不变(`name()` / `priority()` / `version()` / `create(AgentConfig)` 公开方法签名 0 改动,只删 `AllowAllPermissionPolicyProvider` 类级别 `@Component` 注解 1 处)+ `PermissionPolicyRouter` 行为不变(Story #029 已用 v1.5.28 多 Provider 模式)+ `PermissionPolicyAutoConfiguration` SPI 不变(只新增 1 个 `@Bean` 方法)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 9 Slot 体系不变 / JDK 8 兼容(`getDeclaredField` / `getDeclaredAnnotation` / `setAccessible` 已锁,no `var` / `List.of` / sealed / records);**0 新 Maven 依赖** / **0 新 ErrorCode** / R-13 mitigation (d) baseline 镜像 **第 23 次 PASS 0 binary delta**;**累计 40 个 Story 合入**;**业务价值** —— **3 Provider 注册路径 100% 对齐 v1.5.28 §5.5 多 Provider 模式**(Story #001 残留的 `@Component` 单 Provider 假设彻底消除,3 Provider 命名风格一致 `permissionPolicyProvider_<name>-<version>`)+ **v1.5.28 §5.5 §5.2 多 Provider Router 假设兑现**(之前 demo-product 测试时实测发现 `AllowAllPermissionPolicyProvider.@Component` 会与 `PermissionPolicyAutoConfiguration.strictPermissionPolicyProvider()` 产生 2 个同类型 Bean,虽功能上仍 work 但风格不一致,本 Story 统一规范)+ **零行为变化**(yolo back-compat —— `permission-policy: default` 仍走 `AllowAllPermissionPolicy`,demo-product / demo-empty `application.yml` 0 改动)+ **0 新测试 fixture 改动**(35 existing test files fixture 不动,9 new case 全部覆盖本 Story 唯一改动点);README.md 「更新日期」段加 🆕 v1.5.53 Story #037 顶部 blockquote + 「核心特性」段补 🆕 PermissionPolicy 3 Provider 多 Provider 模式对齐 v1.5.28 §5.5 bullet + 「Story 路线图」段追加 #037 retrospective(Story #030 demo-product 测试实测发现 + 3 Provider 注册路径 100% 对齐 + 9 case AC 黑盒验证 / 35 existing fixture 0 改动 / R-13 0 binary delta 第 23 次 PASS / 0 新 ErrorCode / 累计 40 个 Story)+ specs/ROADMAP.md 段一 ✅ 已完成加 #037 行(2026-10-02,688 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 23 次 PASS / 0 新 ErrorCode)+ 段二 🟡 待补 #037 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 40 已合 / 0 待补 / +9 新 case(= 688 = 679 pre-#037 chain + 9 新)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#037` 行**第 23 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 22 → **23 个 Story** + dsh §13 v1.5.53 行新增 17 节记录本 Story 完成 + dsh §5.5 末补 Story #037 marker +「对应设计文档」版本号 v1.5.52 → v1.5.53;**修复者** Claude Code(根据用户 2026-10-02 会话反馈,Story #030 实施完跑 demo-product 测试时实测发现 `AllowAllPermissionPolicyProvider.@Component` 与 v1.5.28 §5.5 多 Provider 模式不一致,补 Story #037 tech debt cleanup);v1.3.47(v1.3.47 Story #030 permission-policy-ask-user 文档同步 — dsh v1.5.51 → v1.5.52(§0 标题 v1.5.51 → v1.5.52 + §13 changelog 新增 v1.5.52 行 17 节 + §15.4 P 段 2 号 `LINGS-P02` 新增 + §15.4 P 段 1 号 `LINGS-P01` 复用 cross-ref)+ **`Decision.AskUser` 3rd outcome 真接通** —— 3 stub 删除(`DefaultToolExecutionContext.approval()` / `DefaultToolExecutor` L112-115 / `LinearTurnEngine.dispatchWithPolicy` L437-445)/ `AskUserPermissionPolicy` 5 段决策(deny-list → ask-list → allow-empty → allow-hit → deny-miss)复用 Story #031 `PermissionPatterns.matches()` 3 形式 pattern 通配 / `AskUserPermissionPolicyProvider` SPI(name="ask" priority=10 version="1.0.0")由 `@Component` 自动注册 / `ApprovalRegistry` @Component ConcurrentMap<sessionId+":"+approvalId, Consumer<Decision>> 终局收集器 / `AgentEvent.ApprovalRequired` 3-arg ctor(approvalId UUID + sessionId + Decision ask)替换原 2-arg ctor / `LinearTurnEngine.dispatchWithPolicy` AskUser branch(CompletableFuture<Decision> 阻塞 → registry.register(approvalId, future) → ApprovalRequired 事件发射 → future.complete(decision) 由 ChatController 端点 / `DefaultToolExecutor.dispatch()` AskUser 分支删除抛 stub,改为 unreachable 代码 / `AgentConfig.ToolsConfig` 5-arg → 6-arg ctor 加 `askList: List<String>` / `AgentConfig` 加 `approvalTimeoutSeconds: int` 默认 **0 = 无超时**(Claude Code overnight parity — 企业审批人可能隔夜(下一个工作日)再决定);`>0` 时 N 秒后 `LINGS-P02` Deny / demo-product `ChatController` `POST /api/approvals/{sessionId}/{approvalId}` body `{decision:"allow"|"deny", reason:"..."}` SSE round-trip 端点 ~+55 行 / 23 new case 跨 7 文件(`AskUserPermissionPolicyTest` 5 L1 + `AskUserPermissionPolicyProviderTest` 2 L1 + `ApprovalRegistryTest` 4 L1 + `AgentEventApprovalRequiredTest` 2 L1 + `AgentConfigAskListTest` 3 L1 + `LinearTurnEngineAskUserTest` 3 L2 + `PermissionPolicyRouterAskIT` 3 L2 + 1 L3 demo-product blackbox round-trip) / 684 tests pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core,lingshu-examples/demo-product dependency:tree -Dverbose` diff **仅时间戳不同** = **0 binary delta 第 16 次 PASS**(`ConcurrentHashMap` + `CompletableFuture` + `UUID.randomUUID` + `Collections.emptyList` + `Arrays.asList` + Lombok `@Value` + Spring `@Component` + Jackson `@JsonProperty` kebab-case 已锁 13 项依赖 0 新 binary 引入);**关键不变项** —— `PermissionPolicy` SPI 不变(公开方法签名 `check(Tool, ToolExecutionContext) → Decision` 不变,只新增 1 个真实现 + 1 个 Provider)/ `Decision` 3 子类(Allow / Deny / AskUser)不变(只 StrictPolicy 用到 Allow + Deny,AskUser 现在真接通)/ `AgentConfig` 不可变契约不变(`@Value` + `@Builder` 25 → 26 字段 final,只扩 `askList` + `approvalTimeoutSeconds` 2 字段;`AskUserPermissionPolicy` 构造器收 `AgentConfig` 不收具体字段,`@Value` Lombok 自动 final 注入)/ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)—— `DefaultToolExecutor.dispatch()` AskUser 路径 stub 删除(LinearTurnEngine 已处理)/ `AgentFactory` SPI 不变(@Autowired 6-Router ctor **不动**,`AskUserPermissionPolicyProvider` 由 `@Component` 自动注册,`PermissionPolicyRouter` Story #029 v1.5.28 多 Provider 模式自动接管)/ `Tool` SPI 不变 + `AgentEvent` 基类不变(只 ApprovalRequired 子类 ctor 加 3-arg)/ §4.7 PermissionPolicy / AuditLogger / Cost 域完全兼容 / 9 Slot 体系不变 / JDK 8 兼容(`ConcurrentHashMap` + `CompletableFuture` + `UUID.randomUUID` + `Collections.emptyList` + `Arrays.asList` + Lombok `@Value` + Spring `@Component` + Jackson `@JsonProperty` kebab-case 已锁,no `var` / `List.of` / sealed / records) + Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住) + ReAct Loop 自实现不变(§4.10.1 硬规则 1 守住);**0 新 Maven 依赖** / **1 新 ErrorCode**(`LINGS-P02 PERMISSION_APPROVAL_TIMEOUT`,Permission 域 P 段 2 号,**仅** `approvalTimeoutSeconds > 0` 时触发)/ R-13 mitigation (d) baseline 镜像 **第 16 次 PASS 0 binary delta**;**累计 39 个 Story 合入**;**业务价值** —— **`Decision.AskUser` 3rd outcome 真接通**(3 stub 删除 + LinearTurnEngine 阻塞-解阻塞回合 + ChatController SSE round-trip + ApprovalRegistry 终局收集器 + AgentEvent.ApprovalRequired approvalId 协议场下)/ **Claude Code overnight parity**(`approvalTimeoutSeconds=0` 默认**永不超时**,企业审批隔夜(下一个工作日)决定也能 merge / `>0` 时明确走超时路径 → `LINGS-P02`)/ **R-04 privilege escalation 缓解**(分值 8 —— 用户可配 `permission-policy: ask` + `ask-list: [bash_safe, write_file, web_fetch]` + Agent 调危险 Tool 需 demo-product ChatController 后台 approve/deny + large-scale enable 企业部署)/ **demo-product SSE round-trip 闭环**(前端 SSE 同意变 permanent hang 风险 + 真实 blocking-then-resume 流转路径,面向用户作为可工作的示例);README.md 「更新日期」段加 🆕 v1.5.52 Story #030 顶部 blockquote + 「核心特性」段补 🤝 PermissionPolicy ask 模式已上线 bullet + 「Story 路线图」段追加 #030 retrospective(`Decision.AskUser` 3rd outcome 真接通 3 stub 替换 / Claude Code overnight parity `approvalTimeoutSeconds=0` 默认无超时 / `AskUserPermissionPolicy` 5 段决策 / `ApprovalRegistry` 终局收集器 / `AgentEvent.ApprovalRequired` approvalId UUID + sessionId / `LinearTurnEngine.dispatchWithPolicy` AskUser branch CompletableFuture 阻塞-解阻塞 / `DefaultToolExecutor.dispatch()` AskUser 分支删除 / `ToolsConfig.askList` / `approvalTimeoutSeconds: int` / demo-product `POST /api/approvals/{sessionId}/{approvalId}` SSE round-trip / 23 case AC 黑盒验证 / R-13 0 binary delta 第 16 次 PASS / +LINGS-P02)+ specs/ROADMAP.md 段一 ✅ 已完成加 #030 行(2026-10-02,684 pass / 0 fail / 2 MCP heartbeat flake pre-existing / R-13 0 binary delta 第 16 次 PASS / +LINGS-P02)+ 段二 🟡 待补 #030 划掉 + 段五 🎯 实施节奏 21. Story #030 已合 + 统计 22 已合 / 0 待补 / +23 新 case(= 684 = 661 pre-#030 chain + 23 新)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#030` 行**第 16 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 21 → **22 个 Story** + dsh §13 新增 v1.5.52 行(后者 17 节记录本 Story 完成)+ dsh §15.4 P 段 L02 = PERMISSION_APPROVAL_TIMEOUT(Permission 域 P 段 2 号)+ dsh §15.4 P 段 L01 = TOOL_NOT_IN_ALLOWLIST(Permission 域 P 段 1 号,Story #029 L02 → L01 编号 0 移位,**仅新增 L02**)+「对应设计文档」版本号 v1.5.51 → v1.5.52;**修复者** Claude Code(根据用户 2026-10-02 会话反馈「我在用 Claude Code 的时候有些需要人审批的,过一个晚上还可以审批后继续流转,难道这个超时时间是有个开关的,或者配置成0默认不超时?」+「实施吧」,触发本 Story 实施 + 23 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 16 次 PASS 0 binary delta**);v1.3.46(v1.3.46 Story #034 a2a-http-domain-guard 文档同步 — dsh v1.5.50 → v1.5.51(§0 标题 + §13 v1.5.51 行 18 节 + §15.9 S 域 cross-ref 复用 #028 `LINGS-S01`)+ **A2A HTTP transport 沙箱守卫落地** —— 完全复用 #033 `McpHttpSupport.checkOrThrow(url, whitelist)` 静态 helper,**2 hook point**(`fetchCard` + `jsonRpcCall`)在 `HttpJsonRpcA2aTransport` 发 HTTP 请求**之前**守卫;`AgentRef.@Value` 扩 `List<String> domainWhitelist` 字段(per-remote-agent 粒度,yml `domain-whitelist: [host1, ...]` kebab-case 绑定)+ `getDomainWhitelistOrEmpty()` null-safe accessor(strict mode 镜像 #033);`HttpJsonRpcA2aTransport` 5-arg ctor + defensive copy + 4-arg ctor 保留 back-compat;`HttpJsonRpcA2aTransportFactory` 内嵌 static class + `@Bean(name="a2aTransportFactory_http-jsonrpc")` 启动期 union `cfg.a2a.remoteAgents[*].domainWhitelistOrEmpty()` 走 `HashSet<String>` 去重传给 transport;`RemoteAgentToolAutoConfiguration.remoteAgentTool()` 注入 factory,`if (transportName=="http-jsonrpc-1.0.0") transport = httpJsonRpcFactory.build();`;**13 new case** 跨 4 文件(`HttpJsonRpcA2aTransportCheckOrThrowTest` 6 L1 + `HttpJsonRpcA2aTransportDomainGuardIT` 3 L2 + `HttpJsonRpcA2aTransportAutoConfigurationTest` +1 L1 + `HttpJsonRpcA2aTransportTest` 3 fixture update);**673 pass / 2 MCP heartbeat flake pre-existing** / R-13 mitigation (d) baseline 镜像 pre/post dep-tree 仅时间戳差异 = **0 binary delta 第 22 次 PASS**(`URI.create` + `List.contains` + `HashSet` + `ArrayList` + `Collections.emptyList` + `Arrays.asList` JDK 8 内置 0 新 binary 引入);**关键不变项** —— `A2aTransport` 5 方法 SPI 不变 / `A2aTransportRouter` 不变 / `RemoteAgentTool` 不变(只看 `A2aTransport` 接口)/ `McpHttpSupport.checkOrThrow` 公开方法不变 / `AccessDeniedException[LINGS-S01]` ErrorCode 复用 / `Tool` SPI 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)/ `AgentConfig` 不可变契约不变(只 `AgentRef` 内部加字段)/ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)/ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 9 Slot 体系不变 / JDK 8 兼容(`URI.create` + `List.contains` + `HashSet` + `ArrayList` + `Collections.emptyList` + `Arrays.asList` + Jackson `@JsonProperty` kebab-case 已锁,no `var` / `List.of` / sealed / records);**0 新 Maven 依赖** / **0 新 ErrorCode**(复用 #028 `LINGS-S01`)/ **累计 38 个 Story 合入**;**业务价值** —— A2A 路径 sandbox 边界守卫到位(与 MCP HTTP 对齐,Agent 调任何远端 A2A server 必须先过 `WhitelistedHttpClient` 沙箱)/ `WhitelistedHttpClient` 基建从 idle → enforced(一路通过 #028 → #033 MCP + #034 A2A 真正在 HTTP 请求路径上 enforce)/ per-remote-agent 粒度配置(不同 remote agent 不同 domain-whitelist,A2A 多 server 部署友好);**修复者** Claude Code(根据用户 2026-10-01 会话反馈「a2a的网络连接也和MCP的网络链接一样使用的了whitelist做校验吗」+「走 MCP 复用模式」+「实施吧」,触发本 Story 实施 + 13 case AC 黑盒验证 + R-13 mitigation (d) baseline 镜像 **第 22 次 PASS 0 binary delta**);v1.3.45(v1.3.45 Story #033 mcp-http-domain-guard 文档同步 — dsh v1.5.49 → v1.5.50(§0 标题 v1.5.49 → v1.5.50 + §13 changelog 新增 v1.5.50 行 17 节记录 Story #033 完成:**Path B + Mitigation 1 沙箱守卫深度** —— MCP HTTP 2 transport(`SseMcpServerConnection` / `StreamableHttpMcpServerConnection`)继续走 raw JDK `HttpURLConnection`(避免 MCP 协议层破坏 + 复用 #021c `McpHttpSupport` JSON-RPC 样板);`McpHttpSupport.checkOrThrow(String url, List<String> whitelist)` 静态 helper(JDK `URI.create(url).getHost()` + `whitelist.contains(host)`)+ **12 hook 点**(SSE 7 + Streamable HTTP 5)在 MCP 发 HTTP 请求**之前**调,失败抛 `AccessDeniedException`(`[LINGS-S01]` 前缀由 super 自动嵌入,真实请求**不**发起);`McpServerConfig.@Builder.Default List<String> domainWhitelist = new ArrayList<>()` + 构造器 `defensive copy new ArrayList<>(cfg.getDomainWhitelist())`;`StdioMcpServerConnection` 0 改动(stdio 走子进程 IPC 不走 HTTP);13 new case 跨 3 文件(`McpHttpSupportCheckOrThrowTest` 8 L1 `checkOrThrow` 单元:emptyWhitelist / nullWhitelist / matchingHost / nonMatchingHost / nullOrEmptyUrl / malformedUrl / caseSensitive / ipv4Host + `McpHttpDomainGuardIT` 4 L2 whitelisted reachesConnected / nonWhitelisted callToolDenied / nonWhitelisted accessDeniedThrown + streamableHttp nonWhitelisted + `McpServerConnectionFactoryTest` +1 L1 factory dispatch create_sse_domainWhitelistPropagated);35 existing test files updated(`.domainWhitelist(Arrays.asList("127.0.0.1"))` + `import java.util.Arrays;` 让 SDK IT fixture 走沙箱白名单,真实 localhost fixture host 不被 deny);660 pass / 2 MCP heartbeat flake pre-existing;`mvn -pl lingshu-core dependency:tree` pre/post diff 仅时间戳差异 = 0 binary delta(`URI.create` + `List.contains` + `ArrayList` + `AccessDeniedException`(`#028` 已落)+ JDK 8 standard 全无新 binary 引入);`banned-dependencies` enforcer Rule 0 passed;**0 新 Maven 依赖** / **0 新 ErrorCode** 复用 #028 `LINGS-S01`(R-13 mitigation (d) 路径**第 18 次验证 PASS**);**0 SPI 改动** —— `McpServerConnection` interface / `A2aTransport` SPI / `Tool` SPI / `RuntimeSandbox` interface / `WhitelistedHttpClient` class / `McpTransport` / `StdioMcpServerConnection` / `McpServerConnectionFactory.create()` 全部 0 改动;`McpHttpSupport.checkOrThrow` 只调 `whitelist.contains(host)` 不创建 full client(避免无谓 WhitelistedHttpClient 实例化);`AgentConfig` 不可变契约不变(0 字段新增;yml `agent.mcp.servers[*].domain-whitelist` 接线留 OQ-Future,hand-rolled `parseMinimalYaml` 不解析嵌套 list,whitelist 只可编程设置通过 `McpServerConfig.builder().domainWhitelist(...)`)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ 9 Slot 体系不变 + JDK 8 兼容(`URI.create` + `List.contains` + `ArrayList` + `Collections.emptyList()` 已锁,no `var` / `List.of` / sealed / records);**累计 22 个 Story 全 0 binary delta 通过**;README.md 「更新日期」段加 🆕 v1.5.50 Story #033 顶部 blockquote + 「核心特性」段补 🛡️ MCP HTTP 域守卫已上线 bullet + 「Story 路线图」段追加 #033 retrospective(Path B + Mitigation 1 架构 + 12 hook 点 SSE 7 + Streamable HTTP 5 + `McpServerConfig.domainWhitelist` + 构造器 defensive copy + 13 case AC 黑盒验证 / 35 existing test files updated / R-13 0 binary delta 第 18 次 PASS / 0 新 ErrorCode)+ specs/ROADMAP.md 段一 ✅ 已完成加 #033 行(2026-10-01,660 pass / 2 MCP heartbeat flake / R-13 0 binary delta 第 18 次 / 0 新 ErrorCode)+ 段二 🟡 待补 #033 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 37 已合 / 0 待补 / +13 新 case(= 660 = 647 pre-#033 chain + 13 新)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#033` 行**第 18 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 21 → **22 个 Story** + dsh §13 v1.5.50 行新增 17 节记录本 Story 完成 + dsh §6.5 (2.1) `McpHttpSupport` 段补 `checkOrThrow` 静态方法契约 + §6.5 (2.1) `SseMcpServerConnection` / `StreamableHttpMcpServerConnection` 段补 12 hook 点说明 + §15.9 MCP ErrorCode 域 cross-ref 加「🆕 v1.5.50 Story #033 复用 LINGS-S01」blockquote +「对应设计文档」版本号 v1.5.49 → v1.5.50;**修复者** Claude Code(根据用户 2026-10-01 会话反馈「开始 lingshu 工程的 Story #033」+ Story #033 spec/plan/tasks 已审批通过 + Path B + Mitigation 1 架构校对后展开实施);v1.3.44(v1.3.44 Story #032 web-fetch-local-tool 文档同步 — dsh v1.5.48 → v1.5.49(§0 标题 v1.5.48 → v1.5.49 + §13 changelog 新增 v1.5.49 行 15 节记录 Story #032 完成:**Claude Code parity primary rationale** —— Claude Code 工具表 `Read / Write / Edit / Glob / Grep / Bash / WebFetch / WebSearch / Task / Skill` 中 `WebFetch` 是 built-in(`Read` 同级),LingShu `ReadTool`(本地 fs)↔ `BashTool`(本地 process)↔ `WebFetchTool`(本地 http)三件套**对称**;MCP fetch(`McpServerConnection` + `McpToolAdapter`)作为「高级 HTTP」共存非互斥(类比 Claude Code WebFetch + MCP fetch 双轨);**GET-only 范围锁定** —— `inputSchema` 固定 `{url: string, max_bytes?: integer}`,POST/PUT/DELETE 业务方走 MCP fetch server;`WebFetchTool implements Tool`(`name()="web_fetch"` snake_case + `description()` 含 "domain whitelist" + "POST/PUT/DELETE traffic is NOT supported" + `inputSchema` 静态 JSON Schema `{url: string, max_bytes?: integer}` + `sourceCategory()="local"` 默认 0 override + `execute(call, ctx)` 4 段(url 校验 → max_bytes 解析 → `ctx.http().get(url)` 委托 `WhitelistedHttpClient` → 1 MB truncation marker `\n...[truncated, original %d bytes]`)+ 2 catch(`AccessDeniedException` → `ToolResult.error("[LINGS-S01] Domain not whitelisted: <url>")` 复用 #028 / `IOException` → `ToolResult.error("HTTP fetch failed: <msg>")` 无 ErrorCode 前缀)+ `LocalToolsAutoConfiguration` **0 改动**(`Map<String, Tool> tools` Spring 自动注入所有 `@Component implements Tool` 5 个 Tool 已就位(`"Read"` / `"Write"` / `"Edit"` / `"Bash"` / `"web_fetch"`)) + HTTPS 透明(JDK `HttpsURLConnection` 自动按 scheme 选实现,User-Agent `"ChaOS-LingShu-Sandbox/1.0"` 透传);15 new case 跨 4 文件(`WebFetchToolTest` 8 L1 metadata + execute / `WebFetchToolHttpServerIT` 4 L2 真发请求 + HTTPS + 404 / `LocalToolsAutoConfigurationWebFetchIT` 2 L2 装配 wiring / `DemoProductWebFetchIT` 1 L3 yml 端到端 localhost allow + example.com deny 双半);`mvn -pl lingshu-core dependency:tree` pre/post diff 仅时间戳差异 = 0 binary delta(`@Component` + JDK `HttpURLConnection` / `HttpsURLConnection` + JDK `com.sun.net.httpserver.HttpServer` IT fixture + Jackson `ObjectNode`/`JsonNode` + Mockito + AssertJ 全 JDK 8 standard + 已锁 13 项依赖表内,无新 binary 引入);**关键不变项** —— `Tool` SPI 不变(`sourceCategory()` 是 #031 加的 default 方法,本 Story 不 override 走默认 `"local"`)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `WhitelistedHttpClient` SPI 不变(#028 已落,本 Story 是 idle 基建激活)+ `RuntimeSandbox.http()` 不变(#028 已落)+ `AccessDeniedException[LINGS-S01]` ErrorCode 不变(#028 已落,本 Story 复用)+ `LocalToolsAutoConfiguration` 0 改动(`Map<String, Tool> tools` autowiring 自动接住)+ `AgentConfig` 不可变契约不变(0 字段新增)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ 9 Slot 体系不变 + JDK 8 兼容(JDK `HttpURLConnection` + `HttpsURLConnection` + `com.sun.net.httpserver.HttpServer` + `Collections.emptyList()` + `Arrays.asList()` 已锁,no `var` / `List.of` / sealed / records);**0 新 Maven 依赖** / **0 新 ErrorCode**;README.md 「更新日期」段加 🆕 v1.5.49 Story #032 顶部 blockquote + 「核心特性」段补 🌐 WebFetch 本地 HTTP/HTTPS 抓取 Tool 已上线 bullet + 「Story 路线图」段追加 #032 retrospective(Claude Code parity 三件套对称 + GET-only 范围锁定 + 1 MB truncation marker + WhitelistedHttpClient 沙箱守卫激活 + HTTPS 透明 + 15 case AC 黑盒验证 + R-13 0 binary delta 第 17 次 PASS)+ specs/ROADMAP.md 段一 ✅ 已完成加 #032 行(2026-10-01,647 pass / 2 MCP heartbeat flake / R-13 0 binary delta 第 17 次 / 0 新 ErrorCode)+ 段二 🟡 待补 #032 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 36 已合 / 1 待补(#033)/ +15 新 case(= 647 = 632 pre-#032 chain + 15 新)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#032` 行**第 17 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 20 → **21 个 Story** + dsh §13 v1.5.49 行新增 15 节记录本 Story 完成 + dsh §6.5 (1) local Tool 段补 WebFetchTool 4 method 契约 + §6.3 `WhitelistedHttpClient` 段 cross-ref 加「🆕 v1.5.49 Story #032 激活」blockquote +「对应设计文档」版本号 v1.5.48 → v1.5.49;**修复者** Claude Code(根据用户 2026-10-01 会话反馈「继续实施吧」+ Story #032 spec/plan/tasks 已审批通过 + Claude Code parity primary rationale 校对后展开实施);v1.3.43(v1.3.43 Story #031 permission-policy-pattern-matching 文档同步 — dsh v1.5.47 → v1.5.48(§0 标题 v1.5.47 → v1.5.48 + §13 changelog 新增 v1.5.48 行 16 节记录 Story #031 完成:**Story #029 follow-up #1 实测发现 demo yml `allow-list: [read_file, write_file, list_dir, bash_safe]` 静态枚举 brittleness 根治** —— 引入三形式 Pattern 通配语法替代 `StrictPermissionPolicy.check()` 内 12 行硬编码 `name.equals()` brittleness:**Pattern grammar** —— `*` 通配所有 tool / `<exact-name>` 精确名匹配 / `<category>:*` 类别前缀(`local` / `mcp` / `skill` / `a2a` / `delegate` 5 类,§4.6 `Tool.sourceCategory()` 默认方法提供);**`Tool.sourceCategory()` 默认方法** —— `Tool` SPI 加 1 个 `default String sourceCategory() { return "local"; }`(§4.6 不变,5 类别枚举契约),`McpToolAdapter` / `SkillTool` / `RemoteAgentTool` / `DelegateTool` 各自 override 报 `mcp` / `skill` / `a2a` / `delegate`;**`StrictPermissionPolicy.check()` 重写** —— `deny-list 命中 → Deny + 嵌 [LINGS-P01]`(优先级最高)/ `allow-list 空 → Allow`(默认全开,yolo back-compat)/ `allow-list 任一项匹配 → Allow`(三种 pattern 形式 OR)/ `allow-list 非空 + 全不命中 → Deny + 嵌 [LINGS-P01]`;**`StrictPermissionPolicyProvider.create(AgentConfig)`** —— 启动期构造 `Map<String, String> nameToCategory = ToolRegistry.findAll().stream().collect(toMap(t -> t.name(), t -> t.sourceCategory()))`,注入到 `StrictPermissionPolicy`(@Value Lombok 自动 final 注入,`PermissionPolicy` SPI 公开方法签名不变);**`StrictPermissionPolicy` NOT @Component** —— 关键修复:`@Component` on value-object policy class 会让 Spring reflection instantiate 无参 ctor,本 Story 改用纯 POJO 模式,由 `StrictPermissionPolicyProvider.create()` 显式 new 注入 `nameToCategory` Map(§5.5 多 Provider 模式 name-based resolve);**11 file implementation breakdown** —— 1 new `ToolSourceCategory` 5 类常量(`ai.lingshu.core.permission`)/ 1 modify `Tool.java` 加 `sourceCategory()` default 方法 + Javadoc / 4 modify `McpToolAdapter` / `SkillTool` / `RemoteAgentTool` / `DelegateTool` override `sourceCategory()` / 1 modify `StrictPermissionPolicy.java` 全量重写 `check()`(~85 行)/ 1 modify `StrictPermissionPolicyProvider.java` 新增 `ToolRegistry` 依赖 + `create()` body / 1 new `StrictPermissionPatternMatcher` 静态 helper(`matches(pattern, toolName, nameToCategory)` 三段 OR 短路)/ 1 modify `McpServerProperties.java` 加 `pattern` 字段(可选 yml 扩展点,本期 unused)/ 1 new `PermissionPolicyYamlPermissionPolicyIT` L3 集成 / 1 modify `DemoProductApplication.java` `agent.tools.allow-list` 支持 pattern 形式;**26 new case 跨 8 文件** —— `StrictPermissionPatternMatcherTest` 12 L1(空 pattern 抛 IAE / 5 形式优先级 / 通配符优先级 / 类别前缀 / 精确名 / 混合 OR 短路 / null 防御 / 大小写敏感 / 嵌套 pattern 不展开 等)/ `StrictPermissionPolicyPatternTest` 6 L1(yolo back-compat + 三段决策 + 嵌 ErrorCode + category 通配)/ `StrictPermissionPolicyProviderPatternTest` 3 L2(`create()` inject `nameToCategory` + multi-tool registry)/ `PermissionPolicyRouterPatternIT` 2 L2(strict + default 共存 + 实际 dispatch)/ `AgentFactoryYamlPatternIT` 3 L2(yml pattern binding 端到端)/ `ToolSourceCategoryDefaultTest` 2 L1(5 类别 override 验证)/ `McpToolAdapterSourceCategoryTest` 1 L2(MCP tool 报 mcp 类别)/ `SkillToolSourceCategoryTest` 1 L2(Skill tool 报 skill 类别);**647 pass / 0 fail / 2 MCP heartbeat flake pre-existing**(0 新 flake)/ **0 新 ErrorCode**(复用 LINGS-P01,不动域字母表)/ R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree` diff 仅时间戳差异 = **0 binary delta 第 16 次 PASS**(纯 JDK 8 `String.equals` + `String.startsWith` + `Map.get` + `ConcurrentHashMap` 已锁 + Lombok `@Value` + Spring `@Bean(name = "permissionPolicyProvider_strict")` 0 新 binary 引入);**关键不变项** —— `PermissionPolicy` SPI 不变(只 `StrictPermissionPolicy.check()` 实现层重写,公开方法签名不变)+ `Decision` 3 子类不变(只 Strict 实现用到 Allow + Deny)+ `Tool` SPI 不变(`sourceCategory()` 是 default 方法,所有现有 Tool 实现自动继承 `local` 0 改动)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `AgentConfig` 不可变契约不变(只 `ToolsConfig.allowList` 改语义,字段不变)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor **不动**)+ §4.7 PermissionPolicy 整体架构不变 + AuditLogger / Cost 域完全兼容 + 9 Slot 体系不变 + JDK 8 兼容(`ConcurrentHashMap` / `String.equals` / `Collections.emptyList()` 已锁,no `var` / `List.of` / sealed / records)/ **0 新 Maven 依赖** / **0 新 ErrorCode** / R-13 mitigation (d) baseline 镜像 **第 16 次 PASS 0 binary delta**;**OQ-9 follow-up** —— `StrictPermissionPolicy.check()` 暂不实现 **AskUser** 决策路径(§4.7 未来 RFC);**yolo path back-compat** —— `permission-policy: default` 仍走 `AllowAllPermissionPolicy`;**Story #029 后续 gap 根治** —— 静态枚举 [read_file, write_file, ...] brittleness(`bash_safe` / `agent_card_fetch` 等 Skill/MCP/A2A 工具新加时必须修改 yml)根治,pattern 通配符 `mcp:*` / `skill:*` 即可覆盖;README.md 「更新日期」段加 🆕 v1.5.48 Story #031 顶部 blockquote + 「核心特性」段补 🎯 PermissionPolicy 三形式通配 bullet + 「Story 路线图」段追加 #031 retrospective(11-file 实现 / 26 case AC 黑盒验证 / R-13 0 binary delta 第 16 次 PASS / Story #029 静态枚举 brittleness 根治)+ specs/ROADMAP.md 段一 ✅ 已完成加 #031 行(2026-10-01,647 pass / 2 MCP heartbeat flake / R-13 0 binary delta 第 16 次 / 0 新 ErrorCode)+ 段二 🟡 待补 #031 划掉(全部 18 个 Story 已合 🎉)+ 段五 🎯 实施节奏 🎉 收口 + 统计 35 已合 / 0 待补 / +26 新 case(= 647 = 621 pre-#031 chain + 26 新)+ `constitution.md` §10 R-13 缓解 Story 列表补 `#031` 行**第 16 次** R-13 mitigation (d) PASS 0 binary delta + 累计计数 19 → **20 个 Story** + dsh §13 v1.5.48 行新增 16 节记录本 Story 完成 + dsh §4.6 `Tool` 接口补 `sourceCategory()` default 方法 Javadoc + §5.5 Slot 4 design intent 段补 Story #031 marker +「对应设计文档」版本号 v1.5.47 → v1.5.48;**修复者** Claude Code(根据用户 2026-09-30 会话反馈「继续实施吧」+ Story #031 spec/plan/tasks 已审批通过);v1.3.42(v1.3.42 Story #029 permission-policy-impl 文档同步 — dsh v1.5.46 → v1.5.47(§0 标题 v1.5.46 → v1.5.47 + §13 changelog 新增 v1.5.47 行 14 节记录 Story #029 完成:**OQ-9 解决** —— `PermissionPolicy` SPI 真实现落地:**`StrictPermissionPolicy` @Component @Value`(immutable)+ 3 段决策逻辑(allow-list 空 → 全部 Allow;deny-list 命中 → Deny;allow-list 非空 + 命中 → Allow + allow-list 非空 + 不命中 → Deny 嵌 `[LINGS-P01]` reason)+ `StrictPermissionPolicyProvider` SPI(name="strict" + priority=10 + version="1.0.0")+ `PermissionErrorCodes.LINGS_P01 = "LINGS-P01"`(`P` 域 1 号 = **自 #023 后首次启用新 ErrorCode 域**,域字母表扩展为 11:`C/S/L/T/X/R/A/M/D/P/Z`)+ `AgentConfig.permissionPolicy` 字段扩 + `AgentConfig.ToolsConfig.allowList` / `denyList` 字段扩(空 yml 启动默认 strict 关闭 + allow/deny 空)+ `AgentFactory.loadYamlAndValidate` 加 `permission-policy: strict` top-level key kebab-case 绑定 + `tools.allow-list` / `tools.deny-list` sub-key 绑定(`AgentFactoryYamlPermissionPolicyIT` 3 case 锁定契约)+ `PermissionPolicyRouter` 多 Provider 模式(v1.5.28) name-based resolve(`PermissionPolicyRouterStrictIT` 4 case 含 strict/default 双 Provider 共存 + end-to-end strict deny 嵌入 `[LINGS-P01]`);**18 new case** 跨 4 文件(`StrictPermissionPolicyTest` 8 L1 / `StrictPermissionPolicyProviderTest` 2 L1 / `PermissionPolicyRouterStrictIT` 4 L2 / `AgentFactoryYamlPermissionPolicyIT` 3 L2);**626 pass / 0 fail / 0 MCP flake** / **+1 ErrorCode LINGS-P01**(P 域 1 号,11 域字母)/ R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree` diff 仅时间戳差异 = **0 binary delta 第 15 次 PASS**(纯 JDK 8 `List.contains` + `Collections.emptyList()` + Spring `@Component` + Jackson `@JsonProperty` kebab-case 已锁 0 新 binary 引入);**`yolo` path back-compat** —— `permission-policy: default` 仍走 `AllowAllPermissionPolicy`(demo-product / demo-empty `application.yml` 0 改动);**OQ-9 follow-up** —— `StrictPermissionPolicy.check()` 暂不实现 **AskUser** 决策路径(decision.governance 仍 §4.7 未来 RFC),**未来加**:用户 yml `permission-policy: ask` + 决策走 `Decision.AskUser` 时,`LinearTurnEngine` 异步等 user reply;**README.md**「核心特性」段补 🛡️ PermissionPolicy strict 模式已上线 bullet +「Story 路线图」段追加 #029 retrospective(SPI 扩展 pattern + 3 段决策 + yolo back-compat + 18 case AC 黑盒验证 + R-13 0 binary delta 第 15 次 PASS / OQ-9 follow-up)+ specs/ROADMAP.md 段一 ✅ 已完成加 #029 行(2026-09-30,626 pass / 0 fail / R-13 0 binary delta 第 15 次 PASS / +LINGS-P01)+ 段二 🟡 待补 #029 划掉 + 段五 🎯 实施节奏 🎉 收口 + 统计 34 已合 / 0 待补 / +18 新 case(= 626 = 608 pre-#029 chain + 18 新)+ `constitution.md` §4 域字母 10 → 11 + `P = Permission` 域 LINGS-P01 行 + §10 R-13 缓解 Story 列表补 `#028 + #029` 行**第 15 次** R-13 mitigation (d) PASS 0 binary delta + dsh §15.11 P 段 L01 = TOOL_NOT_IN_ALLOWLIST(Permission 域首条)+ §15.11 顺延为 §15.12 ErrorCode 编码约定;**关键不变项** —— `PermissionPolicy` SPI 不变(只新增 1 个真实现 + 1 个 Provider,`PermissionPolicy.check(ToolCall, ToolExecutionContext) → Decision` 公开方法签名不变)+ `Decision` 3 子类(Allow / Deny / AskUser)不变(只 Strict 实现用到 Allow + Deny,AskUser 路径未启用)+ `AgentConfig` 不可变契约不变(`@Value` + `@Builder` 24 → 26 字段 final;只扩展 +1 top-level 字段 +2 ToolsConfig 字段;`StrictPermissionPolicy` 构造器收 `AgentConfig` 不收具体字段,`@Value` Lombok 自动 final 注入)+ `Tool` SPI 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor **不动**,新 `StrictPermissionPolicyProvider` 由 `@Component` 自动注册,`PermissionPolicyRouter` v1.5.28 多 Provider 模式自动接管)+ §4.10.1 硬规则 2 守住 —— `ToolExecutor.dispatch()` 第一步仍调 `PermissionPolicy.check()`,`StrictPermissionPolicy` 只是替换 `AllowAllPermissionPolicy` 的具体实现 + 9 Slot 体系不变 + JDK 8 兼容(`Collections.emptyList()` / `Arrays.asList()` / Jackson `@JsonProperty` kebab-case 已锁,no `var` / `List.of` / sealed / records)/ **0 新 Maven 依赖** / **1 新 ErrorCode**(LINGS-P01,Permission 域 P 段 1 号,**自 #023 后首次启用新 ErrorCode 域**)/ R-13 mitigation (d) baseline 镜像 **第 15 次 PASS 0 binary delta**;**修复者** Claude Code(根据用户 2026-09-30 会话反馈「实施吧」+ Story #029 spec/plan/tasks 已审批通过,实装 OQ-9 PermissionPolicy SPI 真实现落地);「对应设计文档」版本号 v1.5.46 → v1.5.47);v1.3.41(v1.3.41 🧹 refactor 删除死代码 `Message.ToolUse` — dsh v1.5.45 → v1.5.46(§0 标题 v1.5.45 → v1.5.46 + §4.1 代码示例删 `ToolUse` block + §13 changelog 新增 v1.5.46 行 8 节记录本 refactor:`Message.ToolUse`(`Message.java` L73-84)从未被生产代码 `new` 出来过 —— 历史是 #020a `Skill` / #020c `SkillCommandDispatcher` 早期设计的"独立 `Message.ToolUse` subtype 表示一次 tool_call" 路径,后 #024 / #027a 协议转换层落地后,实际生产链路改走 "`Message.Assistant.toolCalls` 嵌入模式",`Message.ToolUse` 自此变成 0 引用死代码,只剩 `TruncatingCompactor.messageCharLen` 一个 `instanceof` 分支 + 5 处 JavaDoc `@link` 引用;本次直接删除 `ToolUse` nested class + `import com.fasterxml.jackson.databind.JsonNode`(已无人用) + `Message.role()` Javadoc 「5 类」→「4 类」+ `TruncatingCompactor.messageCharLen` 删 instanceof 分支(class-level JavaDoc "assistant+tool_use+tool_result triples" → "Assistant messages + their trailing ToolResult blocks")+ `AnthropicLlmProvider.L383` 注释「Message.ToolUse is not stored in session history」→「tool_use blocks live on Message.Assistant.toolCalls」+ `LingsLlmProviderException` / `LlmErrorCodes` JavaDoc 5 处 `@link ai.lingshu.core.message.Message.ToolUse` → `@link ai.lingshu.core.message.Message.Assistant#toolCalls`;5 文件改动(5 modify 0 new)/ 0 新 Maven 依赖 / 0 新 ErrorCode / **0 测试 case 改动**(全部 583 测试 0 回归,2 MCP heartbeat flake pre-existing 已在 CLAUDE.md 文档化)/ **`mvn -pl lingshu-core dependency:tree` 0 binary delta**(纯 Java 文件内类型清理);**关键不变项** —— `Tool` SPI 不变 / `Message` 5 → **4 子类**(System / User / Assistant / ToolResult,Assistant 仍带 `toolCalls` 字段不变)/ `Message.Assistant.toolCalls` 嵌入契约不变(#024 已落)/ `LlmResponse.toolCalls` 不变(#027a 已落)/ `LinearTurnEngine` 公开方法签名不变(`#027a` 1 行 wire-through 修复已落)/ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)/ `AgentConfig` 不可变契约不变 / `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)/ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 9 Slot 体系不变 / 24 字段 AgentConfig schema 不变 / JDK 8 兼容(`Collections.emptyList()` / `Arrays.asList()` / Jackson 已锁,no `var` / `List.of` / sealed / records);README.md 「更新日期」段加 🆕 v1.5.46 refactor 顶部 blockquote + 「Story 路线图」段 1685/1725 行 `Message` 5 子类 不变 → 5 子类 + 🆕 v1.5.46 refactor 已删除 `Message.ToolUse` 死代码 → 4 子类契约生效 注解 + specs/ROADMAP.md 段二 🟡 待补(空)的 ⚠️ 副作用提示「`Message` 5 子类不变」claim 加 🆕 v1.5.46 refactor 注解 + dsh §13 v1.5.46 行新增 8 节记录(本 refactor 完整内容);**修复者** Claude Code(根据用户 2026-09-30 会话反馈,用户问「`Message.ToolUse` 这个是不是从来没有被用过?」,确认 dead code 后要求「删除吧」);v1.3.40(v1.3.40 Story #027b anthropic-stream-tool-sse 文档同步 — dsh v1.5.44 → v1.5.45(§0 标题 v1.5.44 → v1.5.45 + §13 changelog 新增 v1.5.45 行 16 节记录 Story #027b 完成:**LLM 流式首 token NFR P50 ≤ 1.5s 达标** —— `AnthropicLlmProvider.stream()` 升级非流式 POST+readAll → 真 SSE 流式(`HttpURLConnection` + `setRequestProperty("Accept", "text/event-stream")` + `BufferedReader.readLine()` SSE 行流)+ `AnthropicStreamParser` 6 段状态机(`Map<Integer, TextBlockBuffer>` + `Map<Integer, ToolBlockBuffer>` 累积 `partial_json` + `content_block_stop` 解析 `ObjectNode input`)+ `AnthropicStreamEvent` 不可变事件类(`type` + `JsonNode data` + `static parse(rawSseBlock)` 工厂)+ `LlmErrorCodes.LINGS_L03` reserved 占位(留 §14 N6 graceful shutdown,本期不抛)+ 0 新抛 ErrorCode(L03 reserved 序号占位 + L05 复用邻号 `LLM_RESPONSE_MALFORMED` 实抛 4xx/5xx);**AC 黑盒 8 项** —— AC-NN-1(`Accept: text/event-stream` 头)PASS L2 IT + AC-NN-2(`message_start` 发 `ReasoningStarted`)PASS + AC-NN-3(`text_delta` 累积 + 每 delta 发 `TextDelta`)PASS + AC-NN-4(`input_json_delta` 累积 + `content_block_stop` 解析)PASS + AC-NN-5(multi-block interleaved 状态机)PASS + AC-NN-6(缺 `tool_use.id` 抛 `LINGS-L01`)PASS + AC-NN-7(mock 20 ms 增量 SSE → sink 收 `ReasoningStarted + ToolStarted`)PASS + AC-NN-8(`finish()` 前不发 `message_stop` 抛 `IllegalStateException`)PASS;AC-NN-9 dep-tree 0 binary delta **第 13 次 PASS**(HttpURLConnection + BufferedReader + Jackson 已锁 + `com.sun.net.httpserver.HttpServer` JDK 内置);13 new case 跨 4 文件(`AnthropicStreamEventTest` 2 L1 + `AnthropicStreamParserTest` 6 L1 + `AnthropicStreamProviderIT` 2 L2 + `AnthropicStreamTestSupport` fixture)+ 587 pass(574 pre + 13 新);5 modify + 4 new 源文件(`AnthropicStreamEvent` ~50 + `AnthropicStreamParser` ~180 + `LlmErrorCodes` extend + `AnthropicLlmProvider.doPostStream` 新增 ~120 行)严格 ≤5 边界内;**关键不变项** —— `LlmProvider` SPI 不变(只是 `AnthropicLlmProvider` 实现层扩展,`stream(Prompt, TurnContext, Subscriber<AgentEvent>) → CompletableFuture<LlmResponse>` 公开方法签名不变)+ `Message` 5 子类 + 字段不变(#027a 已落地)+ `Prompt` + `ToolSpec` + `ToolRegistry.modelVisibleSpecs()` 不变(#024 + #020a 已落地)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `Tool` SPI 不变 + `LinearTurnEngine` 公开方法签名不变(#027a 1 行 wire-through 后未改)+ `AgentConfig` 不可变契约不变 + `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ §4.7 PermissionPolicy / AuditLogger / Cost 域完全兼容 + 9 Slot 体系不变 + 24 字段 AgentConfig schema 不变 + JDK 8 兼容(`HttpURLConnection` + `BufferedReader` + Jackson 已锁 + `com.sun.net.httpserver.HttpServer` JDK 内置,no `var` / `List.of` / sealed / records) + Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2 守住 — `AnthropicLlmProvider` 走 raw JDK `HttpURLConnection` SSE,不走 ChatClient 自动执行);README.md 「核心特性」段补 📡 Anthropic SSE 真流式已上线 bullet + 「Story 路线图」段追加 #027b retrospective(8 文件改动 / 13 case AC 黑盒验证 / R-13 0 binary delta 第 13 次 PASS / NFR P50 ≤ 1.5s 达标)+ specs/ROADMAP.md 段一 ✅ 已完成加 #027b 行(2026-09-30,587 pass / 3 MCP heartbeat flake / R-13 0 binary delta 第 13 次 / +LINGS-L03 reserved)+ 段二 🟡 待补 #027b 划掉(全部 16 个 Story 已合 🎉)+ 段五 🎯 实施节奏 🎉 收口 + 统计 32 已合 / 0 待补 / +13 新 case(= 587 = 574 pre-#027b chain + 13 新)+ `constitution.md` §4 L 段 LINGS-L03 reserved 行 + §10 R-13 缓解 Story 列表补 `#027b` 行**第 13 次** R-13 mitigation (d) PASS 0 binary delta + dsh §13 新增 v1.5.44 + v1.5.45 行(后者 16 节记录本 Story 完成)+「对应设计文档」版本号 v1.5.44 → v1.5.45);v1.3.39(v1.3.39 Story #027a anthropic-tool-protocol-conversion 文档同步 — dsh v1.5.43 → v1.5.44(§0 标题 v1.5.43 → v1.5.44 + §13 changelog 新增 v1.5.44 行 13 节记录 Story #027a 完成:**OQ-7 解决** —— `AnthropicLlmProvider.buildRequestBody` 4 段协议转换全贯通(`Prompt.tools` → 顶层 `tools:[]`(`{name, description, input_schema}`)+ `messages[].content` 展开为 array of blocks(`text` / `tool_use` / `tool_result`)+ 连续 `Message.ToolResult` 合并为 1 user message 多 tool_result block Anthropic 协议层硬约束)+ `parseResponse` 解析 `tool_use` block → `ToolCall(id, name, input)` + `TurnContext.appendAssistant` 签名扩 `toolCalls` 参数 + `DefaultTurnContext` 5-arg 实现 + `LinearTurnEngine.L166` 真传 `resp.getToolCalls()`(加 L2-027a 守卫测试 `engine_passesToolCallsThroughToHistoryAssistantMessage`);2 新 ErrorCode `LINGS-L01 TOOL_USE_BLOCK_INVALID` / `LINGS-L02 TOOL_RESULT_BLOCK_INVALID`(LlmProvider 域 L 段 1/2 号,**自 #026 后首次启用新 ErrorCode 域**);**§15.3 L 段编号冲突解决** —— 原 §15.3 L01-L08 占位 8 项为 `LLM_STREAM_*` / `LLM_RATE_LIMITED` / `LLM_AUTH_FAILED` / `LLM_CONTEXT_OVERFLOW` / `LLM_RESPONSE_MALFORMED` / `LLM_COST_BUDGET_EXCEEDED` / `LLM_PROVIDER_UNAVAILABLE` 计划中;Story #027a 落地 L01/L02 = Tool 协议层,**与原 L01/L02 占位冲突 → 原 L03—L10 编号 +2 移位**(原 L03 → L05 / 原 L04 → L06 / ... / 原 L10 → L12)+ 新增 L11—L18 reserved 占位,**8 项原语义不变仅编号位移**;22 新 case 跨 3 文件(`AnthropicLlmProviderTest` 8 L1+L2 + `AnthropicToolReActIT` 2 L3 mock HttpServer round-trip + `LinearTurnEngineToolDispatchTest` +1 L2 wire-through 守卫);574 pass / 3 MCP heartbeat flake pre-existing / R-13 mitigation (d) baseline 镜像 pre/post `mvn -pl lingshu-core dependency:tree` diff 仅时间戳差异 = 0 binary delta **第 12 次 PASS**(Jackson 树已用 + `com.sun.net.httpserver.HttpServer` JDK 内置 + `Arrays.asList` 已锁 全 JDK built-in 无新 binary 引入);`LingsLlmProviderException`(新 file ~25 行)+ ErrorCode 嵌入 message 模式对齐 `LINGS-C0X`(`getMessage()` 前缀 `[LINGS-L0X]` 让 `assertThatThrownBy().hasMessageContaining("LINGS-L0X")` 工作);Reflection 调用私有方法 `Method.setAccessible(true)`(`MaxStepsGuardTest.L311` precedent)+ `catchThrowable()` + `getCause()` 解 `InvocationTargetException` 拿真异常;4 文件改动(3 modify `AnthropicLlmProvider.java` ~105 行新增 + `TurnContext.java` + `DefaultTurnContext.java` + `LinearTurnEngine.java` 1 行 wire-through + 2 new `LlmErrorCodes.java` extend ~30 行 + `LingsLlmProviderException.java` ~25 行);**Anthropic 协议层硬约束 follow-up**:tool_result blocks for a single assistant turn 必须**合并到 1 个 user message** 多 tool_result block 实现("open tool-result user msg" sentinel pattern);README.md 「核心特性」段补 🤖 Anthropic 协议层 Tool 转换已上线 bullet + 「Story 路线图」段追加 #027a retrospective(4 段断链问题 / 根因 / 补丁 4 件套 / 22 case AC 黑盒验证 / R-13 0 binary delta 第 12 次 PASS / Anthropic 协议层硬约束 follow-up)/ specs/ROADMAP.md 段一 ✅ 已完成加 #027a 行(2026-09-29,574 pass / 3 MCP heartbeat flake / R-13 0 binary delta 第 12 次 / +LINGS-L01 + LINGS-L02)+ 段二 🟡 待补 #027a 划掉 + OQ-Future 表 OQ-7 移到 ✅ 已解决表 + 段五 🎯 实施节奏 next = #027b anthropic-stream-tool-sse + 统计 31 已合 / 1 待补 / +22 新 case(= 574 = 552 pre-#027a chain + 22 新 + 后续 0 累)+ `constitution.md` §4 域字母加 `L = LlmProvider` 域 LINGS-L01/L02 行 + §10 R-13 缓解 Story 列表补 `#027a` 行**第 12 次** R-13 mitigation (d) PASS 0 binary delta + dsh §15.3 L 段 L01/L02 = TOOL_USE_BLOCK_INVALID / TOOL_RESULT_BLOCK_INVALID + 原 L03—L10 编号 +2 移位;**关键不变项** —— `LlmProvider` SPI 不变(只是 `AnthropicLlmProvider` 实现层扩展)+ `Message` 5 子类 + 字段不变(只多 1 个 5-arg 构造器,旧 4-arg 构造器保留兼容)+ `Prompt.tools` 契约不变(Story #024 已落地)+ `ToolRegistry.modelVisibleSpecs()` 不变 + `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `Tool` SPI 不变 + `LinearTurnEngine` 公开方法签名不变(只 1 行 wire-through 修复)+ `AgentConfig` 不可变契约不变 + `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 + 9 Slot 体系不变 + 24 字段 AgentConfig schema 不变 + JDK 8 兼容(`Collections.emptyList()` / `Arrays.asList()` / Jackson 已锁 / `com.sun.net.httpserver.HttpServer` JDK 内置,no `var` / `List.of` / sealed / records) / **0 新 Maven 依赖** / **2 新 ErrorCode**(LINGS-L01 / LINGS-L02,LlmProvider 域 L 段 1/2 号,**自 #026 后首次启用新 ErrorCode 域**)/ R-13 mitigation (d) baseline 镜像 **第 12 次 PASS 0 binary delta**;Spring AI `ChatClient.tools().call()` 仍**禁止**使用(§4.10.1 硬规则 2),`AnthropicLlmProvider` 走 raw JDK `HttpURLConnection` POST,不走 ChatClient 自动执行;`#027b`(`anthropic-stream-tool-sse`)流式分支待补);v1.3.38(v1.3.37( — dsh v1.5.41 → v1.5.42(§0 标题 v1.5.41 → v1.5.42 + §13 changelog 新增 v1.5.42 行 13 节记录 Story #023 完成:`SubAgentType` enum 封闭三值 `EXPLORE` / `ENGINEER` / `REVIEWER`(`configKey()` + `promptFile()` + `key()` + `static fromKey` + `static allKeys`)+ `DelegateErrorCodes.LINGS_D01` 单一常量(`D` 域 1 号)+ `SubAgentInheritance.inheritFromParent(parent, skeleton, type)` static helper 手工 24 字段 `AgentConfig` 重组(AgentConfig `@Value` 不可变,**无 toBuilder** → 必须手 new;Delegate child 中**永不为非 null**,**不**递归派孙 Agent)+ `DelegateTool implements Tool`(`name="Task"` 对位 Claude Code 固定名 + `description()` 暴露 closed set + `inputSchema` Jackson `ObjectMapper` 启动期建静态 JSON Schema 含 `enum: [explore,engineer,reviewer]` + `execute()` 4 段防御 + `agentFactory.create(childConfig)` + `child.runBlocking(prompt)` 复用 Story #001 同步路径 + `typeConfigs: Map<SubAgentType, AgentConfig>` 预构建 eager validation startup fail-fast)+ `DelegateAutoConfiguration implements InitializingBean`(3 guard + 1 action,Spring 6 标准做法 commit e13e6a5 fix 替代 `@PostConstruct`)+ 23 new cases 跨 4 测试文件(`SubAgentTypeTest` 3 L1 + `SubAgentInheritanceTest` 9 L1 field-level merge + `DelegateToolTest` 7 L1/L2 Tool SPI 4 方法 + `DelegateAutoConfigurationTest` 4 L2 InitializingBean wiring)/ 1 ErrorCode LINGS-D01 / **536 tests pass / 0 fail** / R-13 mitigation (d) baseline 镜像 pre/post dep-tree **仅时间戳差异** PASS **0 binary delta 第 8 次**(Jackson 已锁 13 项依赖表内 + Lombok + Spring `InitializingBean` transitive 已锁 0 新依赖)+ JDK 23 + Mockito 5.x inline mockmaker 兜底(`StubAgentFactory extends AgentFactory` 子类 bypass 6-Router ctor + 真实 `AgentConfigRegistry.publish/current` 替代 mock) + `ToolResult` 不可变契约不变(`status / toolUseId / content / isError` 4 字段,**无 errorCode** —— LINGS-D01 嵌 `content`)+ 「对应设计文档」版本号 v1.5.41 → v1.5.42 + README.md 「Story 路线图」段追加 #023 retrospective(`SubAgentType` enum + `DelegateErrorCodes` + `SubAgentInheritance` + `DelegateTool` + `DelegateAutoConfiguration` 5 源 + `delegate.types` yml 示例 + Agent.continueWithUserMessageBlocking 集成 + InitializingBean Spring 6 + R-13 0 binary delta 8th PASS)+ specs/ROADMAP.md 段一 ✅ 已完成加 #023 行(2026-09-24,536 pass / 0 fail / R-13 0 binary delta / +LINGS-D01)+ 段二 🟡 待补 #023 划掉(§6 关键实现主链 + 全部并行支链 100% ✅ 庆祝!)+ 段五 🎯 实施节奏 🎉 收口 + 统计 9 已合 / 0 待补 / +598 新 case(= 536 pre-§6 chain + 后 9 已合累)/ §15 域字母加 `D` 行(`D = Delegate` 域 LINGS-D01)+ `constitution.md` §4 域字母 8 → 10 + §10 R-13 缓解 Story 列表补 `#023` 行**第 8 次** R-13 mitigation (d) PASS 0 binary delta;**关键不变项** —— `AgentConfig` 不可变契约不变(`@Value` + `@Builder` 24 字段 final;`SubAgentInheritance` 只读 parent,不改 parent)+ `AgentFactory` SPI 不变(@Autowired 6-Router ctor 不动,只新增 1 个新 client `DelegateTool` 走 `agentFactory.create(childConfig)` 复用)+ `Tool` SPI 不变(DelegateTool implements 现有 Tool 4 方法,无新接口)+ `ToolExecutor.dispatch()` 5 步流水线不变(§4.10.1 硬规则 2)+ `ToolRegistry` SPI 不变(#020a 已落地,DelegateAutoConfiguration 复用)+ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 0 新 Maven 依赖 / 1 新 ErrorCode LINGS-D01(`D` 域 1 号)/ R-13 mitigation (d) baseline 镜像 PASS 0 binary delta);v1.3.36(v1.3.36 Story #022 spring-ai-annotation-tool 文档同步 — dsh v1.5.40 → v1.5.41(§0 标题 v1.5.40 → v1.5.41 + §6.5 (3) L4873-4980 `@AgentTool` 注解 + `SpringAiToolAdapter` + `AgentToolScanner` + `JsonArgsConverter` 完整定义 + §15 域字母加 T 段 8 号空位 = LINGS-T08 反射调用失败 ErrorCode + §13 changelog 新增 v1.5.41 行 13 节记录 Story #022 完成:`@AgentTool` annotation(`@Retention(RUNTIME)` + `@Target(METHOD)` + `name()` + `description()` + `capabilities()` 默认空 + `returnDirect()` 默认 false) + `SpringAiToolAdapter` implements Tool(JSON Schema 启动期 from reflection Method + `execute()` reflection invoke + 业务异常 catch-all 转 `[LINGS-T08]` ErrorCode ToolResult.error)/ `AgentToolScanner` implements `ApplicationContextAware` 启动期扫 `ctx.getBeansWithAnnotation(Component.class).values()` 反射找 `@AgentTool` method 调 `toolRegistry.register(new SpringAiToolAdapter(...))` 完整 LLM 视角可发现 + `JsonArgsConverter.convert(ObjectNode, Parameter[])`(primitive + String 类型映射 / 缺字段 primitive 抛 IAE / boxed 传 null / 复杂类型抛 IAE 走 LINGS-T08)+ `ToolErrorCodes.LINGS_T08` 常量类 + 32 new cases 跨 4 测试文件(13 L1 `JsonArgsConverterTest` + 10 L1/L2 `SpringAiToolAdapterTest` + 5 L2 `AgentToolScannerTest` + 4 L2/L3 `AgentToolIntegrationTest`)/ 1 ErrorCode LINGS-T08 / 513 tests pass / R-13 mitigation (d) baseline 镜像 pre/post dep-tree 仅时间戳差异 PASS 0 binary delta(复用 spring-ai-bom 1.0.0-M6 已在 13 项依赖表内 / Jackson + Lombok 已锁 0 新依赖);**复用 spring-ai `@Tool` 注解信息但不依赖 spring-ai 自动执行**(dsh §4.10.1 硬规则 2 守住 —— Spring AI `ChatClient.tools().call()` 自动执行**禁止**使用)+ specs/ROADMAP.md 段一 ✅ 已完成加 #022 行(2026-09-24,513 pass / 0 fail / R-13 0 binary delta / +LINGS-T08)+ 段二 🟡 待补 #022 划掉 + 段五 🎯 实施节奏 next = #023 delegate-sub-agent(强依赖 #009e 已满足)+「对应设计文档」版本号 v1.5.40 → v1.5.41;**关键不变项** —— `Tool` 接口契约不变(只新增 Tool 实现)/ `ToolRegistry` SPI 不变(#020a 已落地)/ `ToolExecutor` 5 步流水线不变(§4.10.1 硬规则 2)/ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容 / 0 新 Maven 依赖 / 1 新 ErrorCode LINGS-T08(工具域 T 段 8 号空位)/ R-13 mitigation (d) baseline 镜像 PASS 0 binary delta);v1.3.35(v1.3.35 Story #009e a2a-remote-tool-wiring 文档同步 — dsh v1.5.39 → v1.5.40(§0 标题 v1.5.39 → v1.5.40 + §13 changelog 新增 v1.5.40 行 13 节记录 Story #009e 完成 + §5.6.3.1 `HttpJsonRpcA2aTransportAutoConfiguration` stub 同步(v1.5.37 #009c 加的 `remoteAgentTool` + `remoteAgentSchemaBuilder` 2 `@Bean` **已迁出**)+ §5.6.3.2 Grpc/InProcess stub 段 cross-ref 加「🆕 v1.5.40 Story #009e 修复要点」blockquote(3 transport 共享独立 `RemoteAgentToolAutoConfiguration` + `RemoteAgentToolLifecycle` SmartLifecycle 显式 register/unregister,y切换 transport 无需改 Spring config)+ §5.6.4 SPI 总表 Slot 9 行同步 Slot 9 = 🆕 v0.5 + v1.5.37(`RemoteAgentTool`)+ 🆕 v1.5.40 wiring 独立 — `RemoteAgentToolAutoConfiguration` 独立 `@AutoConfiguration` + `RemoteAgentToolLifecycle` 显式 `toolRegistry.register/unregister` + specs/ROADMAP.md 段一 ✅ 已完成加 #009e 行(2026-09-24,333 pass / 0 fail / R-13 0 binary delta / 0 ErrorCode)+ 段二 🟡 待补 #009e 划掉 + 段五 🎯 实施节奏 next = #022 / #023 并行 + 「对应设计文档」版本号 v1.5.39 → v1.5.40)+ **关键不变项** — `RemoteAgentTool` 类**不**改(2/3/5 参构造器全部保留,#009c/#009d 测试 0 regression)/ `RemoteAgentSchemaBuilder` 类**不**改(#009d 已落地)/ `A2aTransportRouter` 行为**不**改(#009a 已落地)/ `A2aTransport` 5 方法契约**不**改/ `ToolRegistry` SPI **不**改(#020a 已落地)/ `ToolExecutor` 5 步流水线**不**改(§4.10.1 硬规则 2)/ §4.7 PermissionPolicy / AuditLogger / Cost 域 完全兼容/ 0 新 Maven 依赖 / 0 新 ErrorCode / R-13 mitigation (d) baseline 镜像 PASS 0 binary delta;**Story #009e 内容** — 抽 `RemoteAgentToolAutoConfiguration` 独立 `@AutoConfiguration`(从 `HttpJsonRpcA2aTransportAutoConfiguration` 删 2 `@Bean` `remoteAgentTool` + `remoteAgentSchemaBuilder` 搬过去)+ 加 `RemoteAgentToolLifecycle implements SmartLifecycle`(`start()` 调 `toolRegistry.register(remoteAgentTool)` `running` flag 幂等保护 + `stop()` 调 `toolRegistry.unregister("remote_agent")` + `isAutoStartup() = true` + `getPhase() = Integer.MAX_VALUE - 1024` 同 `McpTransportLifecycle`)+ `HttpJsonRpcA2aTransportAutoConfiguration` 简化(只保留 `a2aTransportProvider_http-jsonrpc-1.0.0` 1 个 Bean)+ `GrpcA2aTransportAutoConfiguration` / `InProcessA2aTransportAutoConfiguration` 不变(transport Bean 单 Bean 模式)+ SPI 加载顺序 `RemoteAgentToolAutoConfiguration` 行放第一(transport 三行之前,Router 解析时 transport Bean 已就位)+ 14 net new case(5 L1 `RemoteAgentToolAutoConfigurationTest` + 4 L2 `RemoteAgentToolLifecycleTest` + 5 L3 `RemoteAgentTransportWiringIT` - 2 删 `HttpJsonRpcA2aTransportAutoConfigurationTest` 旧 case = 12 净)+ 直接 wiring 不走 `@SpringBootTest`(规避 Mockito 5.x + JDK 23 inline mockmaker 兼容 issue,沿用 Story #007 模式)+ 关键 R-19 (分值 9) RemoteAgentTool wiring gap 本 Story **全部缓解**;**预** Story #022 / #023 之前必做避免后续 patcharound(原计划周期 3 个 Story 排到的 + #009e 实测发现);0 行代码改动(纯文档同步 **预** 真正的实施 commit 由 Story #009e 实施者出);v1.3.34 —(v1.3.34 Story #021c mcp-sse-and-http-transport 文档同步 — README.md 「核心特性」段合并 #021a stdio + #021b tool-adapter + #021c 3 transport 为单 bullet(`McpServerConnection` interface 8 方法 + 6 态状态机 + 3 concrete 实现 stdio/SSE/STREAMABLE_HTTP + `McpServerConnectionFactory.create(cfg.transport())` 静态分派 + `McpHttpSupport` 共享样板 + SSE 手写 parser + streamable HTTP 无状态 POST + 共享指数退避 1s→60s(cap) + 新错误域 M(01/02/03)) +「Story 路线图」段追加 #021c retrospective(`McpHttpSupport` 6 静态方法 + 5 个新 production 文件 + 2 fixture + 1 helper + 10 个新测试文件 + Factory dispatch 改写删 2 旧 case 加 5 新 case + 累计 481 tests 0 fail / R-13 0 binary delta / MCP 3 transport 全上线)/ specs/ROADMAP.md 段一 ✅ 已完成 + 段二 🟡 待补 #021c 划掉 + 段五 🎯 实施节奏 next = #022 / #023 并行 + 统计 6 已合 / 2 待补 / +80 新 case(=481);v1.3.33 Story #021a mcp-stdio-transport 文档同步 — dsh v1.5.37 → v1.5.38(§0 标题 v1.5.37 → v1.5.38 + §13 changelog 新增 v1.5.38 行 13 节记录 Story #021a 完成:`McpTransportType` enum 放 runtime 避免循环依赖 + `AgentConfig.ServerConfig` 扩 5 字段 transport/url/heartbeat*3/reconnectCapMs 向后兼容 + `McpServerConfig` POJO + `ConnectionState` 6 态 + `McpServerConnection` interface 8 方法 + `McpToolDescriptor` / `McpCallResult` / `McpTransportException` 3 最小类型 + `McpServerConnectionFactory` 静态分派(SSE/HTTP 抛 LINGS-M01) + `StdioMcpServerConnection` 完整实现 ~480 行(AtomicReference + daemon ScheduledExecutorService + 5 步 start + 双探活 probe + 指数退避 1s→60s(cap) 无限重试 + per-listener try/catch 异常隔离 + close 优雅停机) + §15.9 MCP 域 LINGS-M01 MCP_CONNECT_FAILED / §15.9 ErrorCode 编码约定 → §15.10 顺延 + §15 域字母加 `M=MCP` / 10 核心 Java 源文件 + 1 modify(AgentConfig.ServerConfig) 边界 stretch 接受(每个文件职责内聚 / 0 共享修改 / R-13 0 binary delta 兜底)/ 36 new cases 跨 12 测试文件 / 1 ErrorCode / 401 tests pass / R-13 mitigation (d) baseline 镜像 pre/post dep-tree 仅时间戳差异 PASS 0 binary delta(JDK 8 内置 ProcessBuilder + BufferedReader + Jackson 已锁 13 项依赖 0 新增))+ README.md 「Story 路线图」段追加 #021a retrospective(`McpServerConnection` interface 8 方法契约 + `ConnectionState` 6 态 + `StdioMcpServerConnection` 完整实现 + `McpServerConnectionFactory` 静态分派 + LINGS-M01 + R-13 0 binary delta)/ specs/ROADMAP.md MCP 支链 A 1/3 完成 🎉(`#021a` ✅ 36 tests 0 fail / R-13 0 binary delta)/ 下一步 = #021b mcp-tool-adapter(McpTransport + McpToolDescriptor + McpCallResult + McpToolAdapter + register/unregister 钩子 + LINGS-M02);v1.3.32 — README.md 「Story 路线图」段追加 #020c retrospective(`SkillCommandDispatcher` 3 段职责 + Agent.continueWithUserMessageBlocking 同步版 + CliRunner /xxx 拦截 + --list-skills banner + Args/ArgsParser flag + R-13 0 binary delta)/ specs/ROADMAP.md 主链 3/3 完成 🎉(Story #020a + #020b + #020c 全部 ✅)/ 下一步 = #021a mcp-stdio-transport;v1.3.31( Story #009d a2a-remote-schema-builder 文档同步 — dsh v1.5.36 → v1.5.37(§0 标题 v1.5.36 → v1.5.37 + §13 changelog 新增 v1.5.37 行 13 节记录 Story #009d 完成:`RemoteAgentSchemaBuilder` 新增 / `AgentRef` 新增放 core / `RemoteAgentTool` 5-arg ctor + description() 3-分支 logic(BASE / HINT / 列表截断)/ `AgentConfig.A2a` 扩 2 字段 `remoteAgents` + `descriptionSkillLimit` / `HttpJsonRpcA2aTransportAutoConfiguration` 加 `@Bean(name="remoteAgentSchemaBuilder")` + 12 测试同步 site / 15 文件改动 / 17 new cases / 0 ErrorCode / 321 tests pass / R-13 mitigation (d) baseline 镜像 pre/post dep-tree 仅时间戳差异 PASS 0 binary delta;OQ-1/OQ-2/OQ-5 仍 OQ-Future)+ CLAUDE.md 对应设计文档版本号引用同步(L218 `dsh_agent_design.md v1.5.36` → `v1.5.37`)+ SKILL.md 同步 v1.0.23 → v1.0.24 + constitution §10 R-13 标记 Story #009d 已缓解 / 0 行代码改动(纯文档同步);v1.3.30 Story #008 react-max-steps 文档同步 — dsh v1.5.35 → v1.5.36(§0 标题 v1.5.35 → v1.5.36 + §0.4 版本 blockquote 预本条 + §6.1 L3611-3686 代码块同步为改后版本 `for (int step = 1; step <= maxSteps; step++)` + 4 条件 AND 守卫发射 + last/totalUsage 跟踪 + `TurnCompleted(last.getStopReason(), totalUsage)` 收口 + §13 表格新增 v1.5.36 行记录 Story #008 完成)+ CLAUDE.md 对应设计文档版本号引用同步(L218 `dsh_agent_design.md v1.5.35` → `v1.5.36`)/ 0 行代码改动(纯文档同步);v1.3.29 Story #007 yaml-hot-reload 文档同步 — dsh v1.5.34 → v1.5.35(§0 标题 + §0.4 版本 blockquote 预本条 + §13 表格新增 v1.5.35 行记录 Story #007 完成)+ CLAUDE.md 对应设计文档版本号引用同步(L217 `dsh_agent_design.md v1.5.34` → `v1.5.35`)/ 0 行代码改动(纯文档同步);v1.3.28 文档一致性同步(SOP v1.17 → v1.18 / prompts v1.0.15 → v1.0.16 / 速览文件 v1.0 → v1.1 / SKILL.md 2 处 4768 → 7250) — v1.3.27 §0 + §13 末尾「对应 SpecKit SOP」/「对应 Prompt 速查」版本号引用 drift(SOP 文件实际 v1.18 但 CLAUDE.md 引用 v1.16 / prompts 文件实际 v1.0.16 但 CLAUDE.md 引用 v1.0.15);`新窗口流程速览(灵枢 Java引擎正式开工).md` 头加 Version 1.1 + 适用前提;dsh v1.5.14 → v1.5.34 / 7250 行;SOP v1.3 → v1.18 / prompts v1.0.2 → v1.0.16;反模式表「Context 爆炸 4768 行」→「7250 行,逼近单次上限」;constitution §3 → §4(错误码约定在 §4,§3 是 NFR 基线);删除末尾残留对话「需要我现在就帮你起...」;**SKILL.md** L24 Harness 硬限制 4768 → 7250 + L169 反模式表 4768 → 7250;**工具完成** `tools/img-pipeline/` 本地不入仓(`doc/公众号系列/*.md` 8 篇 69 块 → 69 PNG + 8 `.wechat.md`,7.2MB;沿用单贡献者 SOP);**关键不变项** — dsh v1.5.34 / v1.5.33 §5.6.3.0 / v1.5.32 §6.4 / v1.5.31 §5.6.3 / 全部 0 行代码改动(纯文档同步 + 工具完成);v1.3.27(依赖同步 — dsh v1.5.33 → v1.5.34(v1.5.34 §7.1 新增子节 —— 明确「AgentFactory 是 Spring 单例 Bean(无状态,持 7 Router),Agent 是 factory 的产品(prototype-like,带 session/config/engine 状态,每次 create 一份,Spring 不持有引用)」 —— **问题** 有读者疑惑「既然 AgentFactory 是 @Component,为什么不把主 Agent 也做成 Spring 单例,直接 @Autowired Agent 拿」 —— **根因** 混淆"基础设施 Bean"与"运行时执行实例"两类对象;**5 维度对比论证** —— (1) 多轮对话 session 隔离(Spring 单例 = 进程一份,session 污染 vs factory 每次 create 独立 session 互不干扰);(2) 子 Agent 共享(主 Agent 单例字段被多个子 Agent 共享引用,父-子状态污染 vs 子 Agent 走同一 factory 独立 config/engine);(3) A2A 多 RemoteAgent(每个 remote agent URL / skill 列表不同塞不进单例 vs §5.6.3.0 AgentCard / AgentRef 本来就是每个 remote agent 独立 config 设计的);(4) 测试 Mock(@MockBean Agent 让所有 @Autowired Agent 拿 mock 包括本该真实的子 Agent vs Mock factory 子 Agent 仍真实);(5) Spring Bean 语义(Spring Bean 默认无状态基础设施,Agent 持 4 个 final 字段运行时才能给值启动期根本塞不进 vs factory 持 7 Router 启动期就绪,Agent final 字段运行时 create() 传入);**Agent 生命周期时序图** —— Spring startup → T0 user code loadYaml + 编辑 cfg → T1 factory.create(cfg)7 项校验 + flowRouter.resolve → T2 agent.run ReAct 循环 → T3 流关闭 / TERM/MAX_STEPS/CANCEL → T4 Agent 实例 GC(无外部引用时 JVM 自处理无需 destroy 钩子)→ T5 session 持久化(按需);**关键不变量**(§4.1 + §7 决策 12 共同保障)—— (1) AgentFactory 整个 JVM 一份(@Component 单例)Agent N 份(每次 create 一份 user code 持有引用);(2) Agent 4 个 final 字段(config/session/engine/toolPool)在 T1 时刻确定 T1→T4 期间不变 §4.1 注释"AgentFactory.create() 构造一次,整个 turn 内不变";(3) Agent 不持有任何 Spring 引用 销毁无需通知 Spring;(4) 单 turn 单 Agent 多 turn 必须多次 create;**扩展点矩阵** —— 7 Router(AgentFactory 直接 @Autowired FlowEngineRouter + SlotResolver 持 6 Router)× 9 Slot,用户扩展 Provider 不扩展 Router / AgentFactory / Agent 三件套,YAML 里 agent.<slot>:<name> 改 Router 解析目标不需要动 Spring 配置;**正确扩展样板** —— @Component public class ParallelTurnEngineProvider implements FlowEngineProvider { name()="parallel" priority()=10 create(cfg)=new ParallelTurnEngine(cfg) };用户拿 Agent 入口永远是 factory.create(...)内部自动选 Provider;**反模式 AgentHolder** —— @Component 包出"单例 Agent"5 个失败场景:(1) session 污染(首次调用的 session 被锁定多用户并发请求全混在一起 §4.7 PermissionPolicy 决策完全失效);(2) config 漂移(用户每次想加 tool 换 model 都不能 AgentHolder 单例创建时已锁死违反 §8 核心原则);(3) 子 Agent 反模式(强制共享 cached 字段污染 或 再开 cached_for_subagent 回归 factory 模式完全多余);(4) A2A 不可行(一个 cached 装不下 N 个 remote agent URL);(5) 违反 §4.1 不变项(把 turn-scoped 升到 process-scoped);**如果真的只有一个永远不变的主 Agent**(简单 CLI 脚本)—— 直接 new AgentFactory(...) + factory.create(cfg)连 Spring 都不用 Spring 单例问题自然消解;**修复者** Claude Code(根据用户 2026-09-17 会话反馈,用户问「AgentFactory 是 @Component,主 Agent 该不该 @Autowired Agent 注入 Bean 容器」,确认设计文档隐含 factory 模式但未显式说明,要求补 §7.1);**§7.1.6 一句话总结** —— AgentFactory 是 Spring Bean(singleton 无状态持 7 Router),Agent 是 factory 的产品(prototype-like 带 session/config/engine 状态每次 create 一份 Spring 不持有引用),用户扩展 9 Slot 的 Provider 不扩展 Agent 本身,拿 Agent 永远走 factory.create(config);**纯文档改动**,代码逻辑零改动;7039 → ~7250 行(+约 210);SKILL v1.0.20 → v1.0.21 / SOP v1.16 → v1.17 / prompts v1.0.14 → v1.0.15 同步(SOP/prompts 本地);v1.5.33 §5.6.3.0 不变;v1.5.33 §5.6.3.0 新增四个核心类型完整定义 —— `AgentCard` Lombok @Data + 4 nested type / `AgentRef` + `AgentRefBuilder` 装配器 priority 去重排序 / `RemoteAgentSchemaBuilder` @Component 启动期扫 `AgentCard.skills[]` 动态生成 `ToolSpec` list / `AgentCardCache` @Component TTL 缓存 + 负缓存 + FIFO evict + 命中率指标 — **问题** v1.5.4 §5.6 引入 A2A 时 4 个核心类型先实现后文档,§5.6.3 L2395-2480 草图引用了但只给名字未给完整定义,**类型契约不在 single-source-of-truth**;**根因** §5.6.3 草图聚焦"3 个新接口 + RemoteAgentTool + AutoConfiguration"简洁契约视图,故意省略"数据类型 + 基础设施 helper"细节层;v1.5.30 §5.6.3.1 补 concrete class 时也只补了 HttpJsonRpcA2aTransport,4 个核心类型未补;**补丁** §5.6.3.0 新增子节(L2482-2992,~510 行),按"**数据 → 引用 → 构建器 → 缓存**"顺序补全:`AgentCard`(Lombok @Data + 4 nested type + 字段语义 Javadoc + `isValid()` 校验)/ `AgentRef`(Lombok @Data + `AgentRefBuilder` 装配器 priority 去重 + 按 priority desc 排序)/ `RemoteAgentSchemaBuilder`(@Component `buildToolSpecs(refs, cards)` 启动期扫 `AgentCard.skills[]` 动态生成 `ToolSpec` list,按 (agentName, skillId) 排序稳定 prompt cache 命中)/ `AgentCardCache`(@Component TTL 缓存 + 负缓存 + FIFO evict + 命中率指标);**关键不变项** —— `A2aTransport` 5 方法契约 / Slot 9 SPI / `A2aTransportRouter` / `RemoteAgentTool` 内部 / `ToolExecutor` 5 步流水线 / §4.7 PermissionPolicy / AuditLogger / Cost 域 全部不变 —— **4 个类型都是"实现细节层",不引入新接口契约**;**纯文档改动**,代码逻辑零改动;6447 → 7039 行(+592);SKILL v1.0.20 → v1.0.21 / SOP v1.16 → v1.17 / prompts v1.0.14 → v1.0.15 同步;v1.5.32 §6.4 Skill 三个契约锚点补全不变;§6.4 Skill 三个契约锚点补全 —— `Skill` 接口定义 + `SkillTool.fromMarkdown` 静态工厂 + `@Component implements Skill` 硬编码对照示例 — **问题** v1.5.31 §6.4 全文写 "Skill extends Tool" 但 §4.6 无 `Skill` 接口定义 + `SkillTool.fromMarkdown(name, content)` 在 L3587/L3636 引用未定义 + 只展示 SKILL.md 文件加载路径缺硬编码 `@Component` 对照路径三重 gap;**根因** §6.4 v1.5.0 写时把 Skill 当 Tool 子集用,缺接口契约 + 工厂方法 + 两种路径对照三件套,Skill 这一支没跟着 v1.5.18 §4.6 ToolExecutor 同步补全;**补丁** §6.4 L3476-3500 新增 `Skill` 接口(`extends Tool` + 4 点差异 + 3 条未来扩展空间)/ §6.4 L3783-3790 `SkillTool` 加 `fromMarkdown(name, markdownContent)` 静态工厂(SKILL.md 第一行 # title 提取 description,inputSchema 固定 `{ "input": string }`)/ §6.4 L3810-3852 新增 `@Component CommitSkill implements Skill` 硬编码对照示例(Conventional Commits 风格 commit message)+ SkillTool vs @Component 对照表 6 行 + 选型决策 3 条;**关键不变项** —— `Tool` 接口 / `SkillLoader` / `CompositeSkillLoader` 行为 / `ToolRegistry` 注册路径 / `ToolExecutor` 5 步流水线 / §4.7 PermissionPolicy / AuditLogger / Cost 域 全部不变;6395 → 6447 行(+52);SKILL v1.0.20 → v1.0.21 / SOP v1.15 → v1.16 / prompts v1.0.14 → v1.0.15 同步;v1.5.31 §5.6.3 L2429-2444 `RemoteAgentToolProvider` 误用 `@AutoService(ToolProvider.class)` 修复 + v1.5.30 §5.6.3.1/§5.6.3.2 不变v1.5.31(§5.6.3 L2429-2444 `RemoteAgentToolProvider` 误用 `@AutoService(ToolProvider.class)` 修复 —— 降级为普通 `Tool` 模式 — **问题** v1.5.30 §5.6.3 用 `@AutoService(ToolProvider.class)` 标注 `RemoteAgentToolProvider` 与 §5.7 SPI 决策(选 Spring Boot SPI 不选 Java SPI)冲突 + §5.5 / §6.5 全部 Tool 用 `@Component` 或 `@AutoConfiguration` + `@Bean` 注册无 `ToolProvider` 抽象 + `ToolProvider` 接口未定义(三重 bug);**根因** v1.5.4 §5.6 引入 A2A 时 RemoteAgentTool 曾短暂被设计为 ToolProvider 抽象,后续降级为普通 Tool 但代码块没同步改;**补丁** §5.6.3 L2429-2444 改写为 `@Component RemoteAgentTool implements Tool` + `RemoteAgentToolAutoConfiguration`(`@Bean public Tool remoteAgentTool()`,与 §5.5 plugin 非 Slot 类型 Bean 样板对齐);§5.6.1 L2346 对照表 Tool 发现机制从「`@AutoService` 启动期静态注册」改「Spring `@Component` / `@AutoConfiguration` + `@Bean` 启动期静态注册」;**关键不变项** —— A2aTransport 5 方法契约不变 / Slot 9 SPI 不变 / A2aTransportRouter 行为不变 / RemoteAgentTool 内部不变(只看 A2aTransport 接口)/ ToolExecutor 5 步流水线不变 / §4.7 PermissionPolicy / AuditLogger / Cost 域 兼容;6377 → 6395 行(+18);SKILL v1.0.19 → v1.0.20 / SOP v1.14 → v1.15 / prompts v1.0.13 → v1.0.14 同步;v1.5.30 §5.6.3.1 + §5.6.3.2 不变v1.5.30 §5.6.3.1 新增 `HttpJsonRpcA2aTransport` concrete class + `HttpJsonRpcA2aTransportProvider` concrete Provider 完整示例 + §5.6.3.2 备选 `GrpcA2aTransport` / `InProcessA2aTransport` 「3 件套模式」扩展指南 — 澄清 `HttpJsonRpcA2aTransport` 是 concrete class 不是 `A2aTransport` 子接口 + 命名不一致修复(匿名 inner class → named class)+ Grpc/InProcess 未来怎么加的 3 件套样板 + application.yml 配置示例 + 实施期检查清单 6 条;6049 → 6377 行(+328);SKILL v1.0.18 → v1.0.19 / SOP v1.13 → v1.14 / prompts v1.0.12 → v1.0.13 同步;v1.5.29 §6.5 (2.1) 新增 `McpServerConnection` 实现示例 (心跳保活 + 指数退避重连) + §6.5 (2) `McpTransport` 同步改写 — §6.5 (2) 假设 MCP server「连上就永远连着」,生产环境 MCP server 子进程可能被 OOM 杀、stdio 僵死、SSE 反向代理超时踢线 —— Agent 进程会因 MCP server 抖动连锁崩盘,且当前文档缺 single-source-of-truth 的心跳 / 重连机制样板,Story #009 实施者只能反推 §4.10.1 错误处理边界自己设计;**根因** §6.5 (2) 原版 `McpServerConnection.start(cfg)` 是一次性同步连接 stub,没引入状态机 / 心跳 / 重连概念;v1.5.x 早期把 MCP 当「远程 Tool 注册中心」轻量集成,没考虑 24×7 长生命周期运维需求;到 v1.5.28 多 Provider 模式 + 9 Slot 体系成熟,**MCP 的「长连接」属性被放大** —— 必须补完整的生命周期管理;**补丁** (1) **§6.5 (2.1) 新增子节**(~270 行):`McpServerConnection` 接口(`extends AutoCloseable`,8 个方法)+ `ConnectionState` enum 6 态 + `McpServerConnectionFactory` 按 transport 分派 + `StdioMcpServerConnection` 完整实现(`AtomicReference` + daemon `ScheduledExecutorService` + 5 步 start + 双探活 probe + 指数退避 `1s → 2s → 4s → 8s → 16s → 32s → 60s(cap)` + 无限重试) + `SseMcpServerConnection` 差异段 + `McpTransport` 同步改写(`McpServerConnectionFactory.create(cfg)` + `onStateChange(listener)` + `onConnectionStateChange()` 私有方法)+ `application.yml` 配置示例 + 启动日志样例;(2) **JDK 8 兼容** —— `AtomicReference` / `Collections.emptyList()`,不用 `List.of` / `var` / sealed;(3) **关键不变项** —— `McpToolAdapter` / `ToolExecutor.dispatch()` / `PermissionPolicy.check()` 完全不变,MCP 断流在 `ToolResult` 层只表现为「error 替代 success」,**不会绕过沙箱 / 权限 / checkpoint 任何一步**,与 §4.10.1 硬规则 2 完全兼容;5709 → 6049 行(+340);SKILL v1.0.17 → v1.0.18 / SOP v1.12 → v1.13 / prompts v1.0.11 → v1.0.12 同步;v1.5.28 §5.5 改「多 Provider 模式」样板 + §5.4 同步改「唯一 Bean 名约定」+ §5.6 Slot 9 `HttpJsonRpcA2aTransportProvider` stub 同步 — **问题** v1.5.27 §5.5 用 `@ConditionalOnMissingBean` 强制「部署期二选一」,同一 Slot 最多 1 个 `XxxProvider` Bean 注册到 Spring 容器 → 用户切换 Provider 必须改 classpath / exclude / 改 Bean 名;但 §5.3.1.0 `SlotRouter<P, T>` 父类**一直是多 Provider 友好** —— 构造器收 `List<P> providers`,启动期按 `name()` 收 `Map<String, P>`,`resolve(name, cfg)` 按 name 选 → List<P> **被设计为 size=N**,而 v1.5.27 `@ConditionalOnMissingBean` 把它阉割到 size=1,**多 Provider 能力框架自身不用**;**根因** v1.5.24 §5.5 引入 6 默认 Provider stub 时直接复用 v1.5.0 `DefaultPromptBuilderProvider` 模板的 `@ConditionalOnMissingBean`,当时设计意图是「防止用户覆盖默认」,但代价是阉割 §5.3.1.0 Router 的多 Provider 能力;v1.5.25 §5.4 双 `@ConditionalOnMissingBean` 模式进一步固化单 Provider 假设(plugin 之间也互斥);到 v1.5.27 §4.6 ToolExecutor + §5.5 默认 6 Provider stub + §5.3.1.0 7 Router 体系成熟,**单 Provider 假设与 Router 多 Provider 设计目标的张力被放大** —— 用户需要「同 Slot 多 Provider 共存 + 按 name 路由」的能力;**补丁** (1) **§5.5 改「多 Provider 模式」样板**:头部设计原则 blockquote 改写,说明 v1.5.28 起默认Provider 用 plain `@Bean(name = "<slot>Provider_<name>")` 而**不再用 `@ConditionalOnMissingBean`**;**所有 6 个默认 Provider stub 改写** —— Slot 1—7 全部 `@Bean(name = "...")` 显式 Bean 名,每段注释补「`name()` 必须唯一(§5.2 同名竞争)」;(2) **§5.5 头部新增「用户切换示例」blockquote** —— `application.yml` 写 `agent.<slot>.name: <provider-name>` 切换 Provider + 启动日志样例 `resolved N provider(s)` 列出全部 N;(3) **§5.5 9-Slot 总表加「Bean 名」列** —— 9 行「Bean 名(🆕 v1.5.28)」字段,如 `promptBuilderProvider_default` / `llmProviderProvider_anthropic` / `flowEngineProvider_linear` / `a2aTransportProvider_http-jsonrpc`,Story 实施者写 `@Bean(name = "...")` 直接抄;Slot 8 / Slot 9 标 🆕 v1.5.28 建议同步改名;(4) **§5.5「替代实现追加约定」段改写** —— 加 `OpenAiLlmProviderProvider` 完整样板(Bean 名 `llmProviderProvider_openai` + name "openai")+ 用户配置示例 + 启动日志样例(4 个 LlmProvider 共存);(5) **§5.6 Slot 9 `HttpJsonRpcA2aTransportProvider` stub 同步改多 Provider 模式** —— `@Bean(name = "a2aTransportProvider_http-jsonrpc")`;(6) **§5.4 plugin AutoConfiguration 编写约定改写** —— 双 `@ConditionalOnMissingBean` 模式 → 唯一 Bean 名约定:🆕 v1.5.28 起 plugin `@Bean` 必须显式 `name = "<slot>Provider_<pluginName>"`,禁止复用默认 Bean 名;🗑️ v1.5.25 双 `@ConditionalOnMissingBean` 模式加载顺序矩阵已废弃,但 `BeanDefinitionOverrideException` 应急路径不变;**效果** Story 实施者写 `@Bean` 时统一规范为 `@Bean(name = "<slot>Provider_<name>")`,无需 `@ConditionalOnMissingBean`;同 Slot 多 Provider 共存由 §5.2 SlotRouter 按 name 路由,`agent.<slot>.name` 改 yaml 即可切换 Provider;§5.3.1.0 Router 多 Provider 能力终于被框架自身利用,**List<P> size=N 实际生效**;5639 → 5709 行(+70));SKILL v1.0.16 → v1.0.17 / SOP v1.11 → v1.12 / prompts v1.0.10 → v1.0.11 同步;§11.6 历史 drift 修正(上轮 v1.3.15)不变))
- **对应设计文档**: `dsh_agent_design.md` v1.5.60
+ **对应设计文档**: `dsh_agent_design.md` v1.5.61
**对应 SpecKit SOP**: `speckit_operator_prompt.md` v1.18
**对应 SKILL**: `lingshu-spec-driven-dev` v1.0.28
**对应 Prompt 速查**: `lingshu_spec_prompts.md` v1.0.16