Commit Graph

1516 Commits

Author SHA1 Message Date
Ma 0da8637099 fix(studio): stop replayed production tasks 2026-08-02 01:09:55 +08:00
Ma efe44dfdbd fix(core): persist chapter state atomically 2026-08-01 23:59:49 +08:00
Ma fc294385f1 fix(studio): close setup and chapter workspace gaps 2026-07-30 00:39:31 +08:00
Ma d08bbed005 test(studio): account for LM Studio provider 2026-07-29 22:11:06 +08:00
Ma 66fad52bc4 fix(studio): widen chapter preview on large screens 2026-07-29 21:51:27 +08:00
Ma cf40d4f923 feat(studio): allow custom cover base URLs 2026-07-29 21:50:58 +08:00
Ma 863d916272 feat(llm): add LM Studio local provider 2026-07-29 21:30:59 +08:00
Ma 7a0b1a7054 feat(studio): add safe chapter rewrite workspace 2026-07-29 21:15:32 +08:00
Ma 66449f05c4 fix(agent): run multi-chapter writes as one sequential task 2026-07-29 19:59:18 +08:00
Ma b0cc9a54fc docs(skill): state memory and credential boundaries 2026-07-28 18:08:17 +08:00
Ma d5ba18ce4d docs(skill): narrow agent-visible maintenance surface 2026-07-28 18:03:43 +08:00
Ma 39b8619628 docs(skill): clarify security boundaries 2026-07-28 17:54:26 +08:00
Ma c7851b94ad chore(release): prepare v1.7.2 v1.7.2 2026-07-28 17:18:39 +08:00
Ma 6e4ce0050e refactor(skills): replace legacy capability system 2026-07-27 12:01:14 +08:00
Ma d674c214cf docs: document AgentSkills compatibility 2026-07-27 00:57:05 +08:00
Ma 41a1138fbd feat(studio): import external skill folders 2026-07-27 00:56:00 +08:00
Ma 4b81fbba77 feat(core): add intent-driven AgentSkills runtime 2026-07-27 00:54:42 +08:00
Ma 1a2fd09b50 chore: remove internal planning documents 2026-07-23 09:20:11 +08:00
Ma f19f428a56 docs: lead v1.7 with model configuration 2026-07-21 12:43:54 +08:00
Ma a713a2e87c docs: consolidate v1.7 highlights v1.7.1 2026-07-20 19:52:12 +08:00
Ma 6a16a61977 chore(release): prepare v1.7.1 2026-07-16 14:08:53 +08:00
Ma 9f1945926e docs: keep product intro above sponsorship 2026-07-16 11:44:37 +08:00
Ma ac7764283b docs: retain Kimi open source partner banners 2026-07-16 02:23:02 +08:00
Ma 6c6574ff52 docs: add Kimi sponsorship banners 2026-07-16 02:21:30 +08:00
Ma d3f6ae7e84 test(core): use shared timeout for root import 2026-07-16 01:27:27 +08:00
Ma eb6c8f88bd fix(core): restore direct terminal tool cards 2026-07-16 01:07:07 +08:00
Ma 6ac708a9c8 docs: document narrative forecast workflow 2026-07-16 00:27:18 +08:00
Ma b98252bb49 feat(studio): compare narrative forecasts in book chat 2026-07-16 00:26:04 +08:00
Ma b38f6039c2 docs: design Studio narrative forecast comparison 2026-07-15 23:20:07 +08:00
Ma 7dce957485 fix(core): fingerprint every context input the narrative forecast reads
根因:buildForecastContext 把 story_frame、volume_map、角色卡、支线看板、
章节摘要等内容读进推演提示词,但 contextFingerprint 只哈希了基准章节号、
story/state/*.json 和四个控制文档。于是修改 story_frame.md 等正史输入后,
get_narrative_forecast 仍然返回 stale:false,旧推演被误判为新鲜。

修复:新增 collectFingerprintFiles,枚举 context builder(含其 helper 的
legacy 回退路径 story_bible.md / volume_outline.md / character_matrix.md)
实际读取的全部输入文件,把「相对路径 + 内容」一起纳入哈希:
- 固定路径:book.json、四个控制文档、chapter_summaries.md、
  subplot_board.md、story_frame.md、volume_map.md 及三个 legacy 回退文件
- 枚举目录:story/state/*.json、story/roles/{主要角色,次要角色,major,minor}/*.md
- 缺失文件不产生条目、空文件产生 ["path",""],因此新增/删除输入文件
  也会改变指纹,且空文件与不存在可区分
- story/runtime/(含推演自身目录)不在枚举范围内,防止自引用
- 保持原有性质:内容哈希不用 mtime,条目按路径排序与读取顺序无关

兼容性影响:旧 forecast.json 里存的是旧算法指纹,在新算法下必然 mismatch,
已有推演会全部判为 stale。这是正确且期望的行为——旧指纹本来就无法证明
这些推演仍然新鲜,无需迁移逻辑。
2026-07-15 19:10:13 +08:00
Ma 7dd07f49e8 test(core): keep narrative forecast tools available while a production task runs
rebase 到 master 后与工具门控(a975fb5)的集成裁决:叙事推演三工具只读写
story/runtime/narrative-forecasts/ 下的非正史产物,不修改正史章节与状态,
不属于门控要拦的会改书工具——suppressProductionTools 期间保留,flag 关闭
后的完整工具表断言同步补上这三个工具。
2026-07-15 16:27:17 +08:00
Ma 148b158348 feat(cli): add inkos forecast create/show/select commands
- commands/forecast.ts:
  - inkos forecast create [bookId] --divergence <text> --branches N
    --horizon N [--model/--llm-base-url/--json]:从当前正史生成非正史
    多分支推演,进度信息在非 JSON 模式下逐条输出
  - inkos forecast show [bookId] <forecastId> --json:读取推演并对照当前
    正史重新校验指纹,正史变化时提示已过期(同时持久化 stale 标记)
  - inkos forecast select [bookId] <forecastId> <branchId> --json:只写
    selected-branch-plan.md,输出明确提示不修改正史
  - bookId 位置参数可省略(复用 resolveBookId 的单书自动识别);show 与
    select 用变长参数手动解析以兼容 RFC 里"可选 bookId 在前"的写法,
    解析函数导出供测试
  - 出错时按 --json 与否输出结构化错误或 logError,并设置 exitCode=1
- program.ts:注册 forecast 命令
- 测试:命令与子命令注册、选项齐全(--divergence 必填)、位置参数解析
  的四种形态与报错
2026-07-15 16:23:10 +08:00
Ma 9e493973e3 feat(core): add narrative forecast agent tools
- agent/forecast-tools.ts:三个 pi-agent 工具
  create_narrative_forecast / get_narrative_forecast / select_narrative_branch,
  TypeBox schema 即文档(每个字段的 description 标注适用工具与默认值),
  参数与 forecast runner 函数签名一一对应,不在 execute 里写死 pipeline
  参数;返回文本包含 forecastId、每个分支的 id/标题/意图匹配分/风险数/
  前提以及下一步操作提示(可操作);错误直接抛出交给 agent loop
- create 通过 pipeline.createAgentContext("forecast", bookId) 取 LLM 上下文,
  外部 AbortSignal 透传给 LLM 调用;在聊天轮内同步执行(Studio 确认式
  后台任务是 v2)
- agent-session.ts:三个工具注册进有书会话的 bookTools;edit 模式保持
  确定性工具集,显式排除三个 forecast 工具(create 会调 LLM)
- 更新 agent-session 工具清单快照测试,新增 forecast-tools 行为测试
  (bookId 与活动书一致性校验、stale 提示、选不存在分支时报错列出可选项)
2026-07-15 16:23:10 +08:00
Ma 7b009ea39d feat(core): add narrative forecast agent, prompts, renderers and runner
RFC #342 v1 的三个操作 create / get / select:
- forecast/prompts.ts:zh/en 双语提示词(系统/用户/修复三个 builder),
  向模型明确要求互斥分支、只输出 JSON、字段结构与合法枚举值
- forecast/agent.ts:NarrativeForecastAgent 继承 BaseAgent,单次调用 +
  一次带校验错误反馈的重试;分支数量与请求不符也算非法输出;两次都
  失败直接抛错,绝不把非法输出往下传
- forecast/render.ts:从 forecast.json 确定性渲染 comparison.md(对比表 +
  分支详情)与 selected-branch-plan.md(含过期警告与"不修改正史"脚注),
  不需要第二次 LLM 调用
- forecast/runner.ts:
  - create:读上下文 → 生成分支 → 赋 branch-N id → schema 校验通过后才写
    story/runtime/narrative-forecasts/<id>/,模型输出非法时不留任何文件
  - get:重算 contextFingerprint,正史变化后把 status 持久化标记为 stale
  - select:只写 selected-branch-plan.md(不改 forecast.json、不碰正史),
    选不存在的分支报错并列出可选分支
- index.ts:导出 forecast 模块公共 API
- 测试:agent 校验重试路径、create/get/select 全流程、兄弟分支隔离、
  正史文件前后快照一致、stale 标记、非法输出零残留、越界参数先于模型
  调用被拒绝
2026-07-15 16:23:10 +08:00
Ma 28a04c0a07 feat(core): add narrative forecast context builder with content fingerprint
- forecast/context-builder.ts:只读地收集正史输入(作者意图/当前聚焦/
  当前状态/伏笔账本/故事框架/卷映射/近期章节摘要/人物关系/支线看板 +
  story/state/*.json),复用 planner-context 与 outline-paths 的既有读取
  函数;空书不会补建缺失的控制文件(区别于 StateManager 的 ensure 流程)
- contextFingerprint 是对正史输入的内容哈希(sha256):基准章节号 +
  story/state/*.json + 四个控制/状态 markdown 的内容,与文件顺序无关,
  不依赖 mtime;正史章节落地、结构化状态或控制文档变化都会改变指纹,
  runtime 下的非正史文件不影响指纹
- renderForecastContextMarkdown 按书籍语言输出 zh/en 上下文 markdown,
  空 section 自动跳过
- 测试:指纹确定性、状态/章节/控制文档变化触发指纹变化、无关文件不触发、
  空书零副作用
2026-07-15 16:23:10 +08:00
Ma 4daa7b8dc8 feat(core): add narrative forecast schema and store
叙事多线推演(issue #342)v1 的数据层:
- forecast/schema.ts:Zod schema 定义 NarrativeForecast / ForecastBranch
  (前提假设、未来章节节拍、人物决策、预计变化、一致性风险、不确定性、
  意图匹配度),分支数限定 2-5,superRefine 拒绝重复 branchId;
  parseForecastModelOutput 解析模型输出(容忍代码围栏/前后缀散文/尾逗号,
  非法 JSON 或 schema 不符直接抛错,不落任何文件)
- forecast/store.ts:确定性本地存储,产物固定在
  story/runtime/narrative-forecasts/<forecastId>/ 下(forecast.json、
  comparison.md、selected-branch-plan.md);时钟与 id 工厂可注入,
  id 冲突自动加后缀;写入前先过 schema 校验,非法数据不产生半成品目录
- 测试:schema 校验边界(分支数/重复 id/越界分数/非法风险类型)、
  store 读写往返、损坏文件报错、markStale 不可变更新、路径安全
2026-07-15 16:23:10 +08:00
Ma 1420040f16 feat(cli): whole-book backup and restore
新增整本书的备份与恢复,解决 issue #339 第三项诉求(agent 改坏后
无处回档):

- 新增 book-backup.ts:备份是把 books/<book-id>/ 整目录复制到
  .inkos/backups/<book-id>/<UTC时间戳>/。备份存放在书目录之外,
  所以备份内容天然不含备份目录自身。时间戳时钟可注入(测试不依赖
  真实时间),同一秒内的多个备份自动加 -2/-3 后缀。恢复前先把当前
  书目录自动备份为 <时间戳>-pre-restore(防手滑),再整目录还原;
  backup-id 是用户输入并拼进路径,校验为单个目录名,拒绝路径分隔符。
- CLI 新增 inkos book backup <book-id>(--list 列出既有备份)和
  inkos book restore <book-id> <backup-id>,均支持 --json,
  人类可读输出按环境语言双语显示(备份要能用于 book.json 已损坏
  的书,所以不读书籍配置来决定语言)。
2026-07-15 16:19:44 +08:00
Ma e25848fd71 feat(core): delete the latest chapter with state rollback
复用审阅"拒绝章节"的回滚机制(StateManager.rollbackToChapter),提供
正式的删除最新章能力,解决用户手动删章要同时清理索引、状态、快照、
runtime 文件的痛点:

- core 新增 deleteLatestChapter():只允许删最新章(删中间章需要重编号
  后续章节并重放状态,v1 明确不支持并在报错里说明);删除前先校验回滚
  目标章的状态快照存在,避免出现改到一半的状态;章节 markdown 移入
  chapters/.trash/(重名自动加 -2/-3 后缀),不做物理删除;然后调用
  rollbackToChapter 一次性回滚 index.json、story 状态、快照、runtime
  产物和 sqlite 记忆索引。
- entity rename 的文件收集跳过点开头目录(如 chapters/.trash),
  回收站内容不再被全书改名波及。
- CLI 新增 inkos chapter delete <book-id> [--chapter N],默认删最新章,
  确认交互沿用 book delete 的 (y/N) + --force 惯例,支持 --json,
  提示语按书籍语言双语显示。

不加聊天 agent 工具:破坏性操作先只开 CLI 入口,聊天入口等有确认卡
语义后再说。对应 GitHub issue #339 的第二项诉求(一键删除章节)。
2026-07-15 16:13:39 +08:00
Ma 9bc68fb489 fix(core): update index word count on chapter patch edits
patch_chapter_text(chapter-local-edit)应用补丁后原本不更新 index.json 里
该章的字数,导致局部编辑后索引字数与章节文件不一致;现在补丁写入后按
replace 路径同样的口径(去掉标题和空白后的字符数)重算并写回。

同时新增显式的字数同步入口,处理用户直接手改章节文件后的对齐:
- core 新增 syncChapterWordCounts():按书籍语言选择计数口径
  (zh_chars / en_words),从章节文件重算全部字数,只在有偏差时写回
  index.json;索引里有记录但文件缺失的章节保持原样并在结果里报告。
- CLI 新增 inkos chapter sync [book-id],支持 --json,人类可读输出
  按书籍语言双语显示。

对应 GitHub issue #339 的第一项诉求(章节字数对齐不自动)。
2026-07-15 16:05:38 +08:00
Ma e1eb12f636 feat(studio): retry the last failed chat message
GitHub issue #335 的"重试按钮"部分(推理强度参数 UI 不在本次范围)。

聊天轮请求失败后(/agent fetch 拒绝、/agent 返回 error 字段、模型返回空
内容、模型未选择、草稿会话落库失败),把这次 sendMessage 的原样参数
(text + options)记录到 SessionRuntime.lastFailedSend;ChatPage 在存在
记录且非聊天流式中时,在输入区上方显示"重试"按钮,点击调用新增的
retryLastSend action 用原参数重发,并在重发前清除记录避免重复点击双发。

不记录的情况:用户主动停止(abortSession 已先把 isChatStreaming 置回
false,各失败出口据此跳过)、确认式生产任务轮(任务失败由任务卡自己
展示)。新一轮发送开始时清除旧记录。

测试:store 层覆盖 fetch 拒绝记录、error 字段记录、重试请求体与首发
一致且记录清除、成功轮无记录、用户停止不记录、无记录时重试为空操作。
2026-07-15 15:48:13 +08:00
Ma a975fb5423 feat(core): gate book-mutating tools while a production task runs
根因:后台生产任务运行期间,同会话并行聊天的 agent 仍持有全部生产工具
(agent-session.ts 的书籍会话工具表),只靠注入 backgroundTaskContext
提示词软约束,没有 host 层的硬门控——模型仍可能在任务运行中并发修改
同一本书。

修法(窄门控):
- core:AgentSessionConfig 加可选布尔 suppressProductionTools。为 true 时
  工具表剔除会创建/修改书籍与产物的生产工具(sub_agent、generate_cover、
  write_truth_file、rename_entity、patch_chapter_text、replace_chapter_text、
  import_chapters);read/grep/ls、research/material 与 propose_action
  保留——propose_action 引发的确认任务在 host 侧已有单任务 409 闸门。
  该布尔纳入 Agent 缓存失效判断(与 backgroundTaskContext 同样处理),
  任务开始/结束时缓存的 Agent 重建、工具表恢复。
- studio server:已判定"本会话任务运行中"并注入 backgroundTaskContext 的
  同一处传 suppressProductionTools: true;任务状态提示词补一句
  "生产类工具已临时不可用,任务结束后恢复"(zh/en 双语)。
2026-07-15 15:36:09 +08:00
Ma bfae2d35cf fix(studio): route id-less and compression events away from background task cards
三个事件路由缺陷(并行聊天与后台任务时互相串排):
- context:compression 处理器忽略服务端已带的 executionId,一律写进当前
  聊天流消息:并行时任务 pipeline 的压缩事件会在聊天消息里长出
  context-* 伪工具卡。
- 无 id 的 log / llm:progress 回退到"最近一张运行中工具卡":聊天轮还没有
  自己的工具卡时会落到后台任务卡上(聊天日志串排进任务)。
- 服务端 broadcast("tool:update") 全仓无前端消费者,是死事件。

修法:
- context:compression 带 executionId 时按 id 定位任务卡,把压缩事件作为
  阶段挂上去(不可变更新);卡不存在时丢弃,绝不写进聊天流消息;无 id
  维持现状(聊天轮自己的压缩展示)。
- 无 id 回退改为"最近一张运行中的聊天轮工具卡":跳过带 background 标记的
  任务卡;跳过后没有可挂的卡时丢弃(任务快照重放会带回任务自己的累积
  日志,不丢信息)。
- 删除两处 tool:update 死广播(确认任务 onUpdate 内与聊天轮
  tool_execution_update 转发),已 grep 确认零消费者、无测试引用。
2026-07-15 15:26:39 +08:00
Ma 29b6af4349 fix(studio): stop appending transcript messages to deleted sessions
根因:deletedSessionIds 只拦了任务快照持久化。任务成功/失败路径的
appendManualSessionMessages 仍会给已删除会话追加助手消息,底层
appendTranscriptEvents 是 mkdir + appendFile,会把已删除会话的 sessions
目录条目和 transcript 文件重新建出来(任务收尾追加正好落在删除之后)。

修法:server.ts 增加 appendSessionMessagesUnlessDeleted 守卫(检查
deletedSessionIds 后再调 appendManualSessionMessages),server 内全部
四处手动追加(外部编辑回复、确认分支的指令预写、成功收尾、失败收尾)
统一走守卫;refreshBookSessionFromTranscript 同样在会话已删除时直接
返回,不再刷新和广播标题。不改 core。

测试:把"删除运行中任务的会话"用例接回真实 transcript 实现与真实
deleteBookSession,任务错误路径走完后断言 transcript 文件不存在、
loadBookSession 返回 null(原用例只查了任务快照文件)。
2026-07-15 15:22:37 +08:00
Ma 0091c3c665 fix(studio): find running tasks in memory before the disk snapshot when deleting or aborting
根因:DELETE /sessions/:id 和 POST /sessions/:id/abort 都靠
loadReconciledTaskSnapshot 从磁盘快照拿 execution.id 再查 controller Map。
但任务的 AbortController 注册(同步)与首次快照持久化(在
executeConfirmedProductionAction 内部)之间有多个 await 间隙,窗口内的
删除/中止从磁盘读不到快照,刚启动的任务被漏掉、停不下来。

修法:reservedProductionSessions 从 Set<string> 升级为 Map<string, string>
(sessionId → taskId;预留时 confirmedTaskId 已生成,两处都在手)。新增
findRunningTaskController:优先查内存预留表拿 taskId → controller,磁盘
快照只作回退(覆盖进程重启后的对账场景)。DELETE 与 /abort scope=all
统一改走该入口;finally 的释放逻辑不变(Map.delete 同 key)。

测试:让任务开始时预写指令的首次 appendManualSessionMessages 挂起,制造
"controller 已注册、磁盘无快照"的窗口,断言窗口内 /abort 返回
aborted: true、DELETE 能让任务的中止信号生效且请求以错误结束。
2026-07-15 15:20:15 +08:00
Ma 28f2923fc0 fix(studio): reclassify a chat turn as a production task when the server starts one
根因:free-text"写下一章"(无 requestedIntent)在服务端经
isWriteNextProductionRequest 命中写章启发式后按后台生产任务执行,但前端
isConfirmedProductionSend 对 requestedIntent === undefined 直接返回 false,
该轮被当成聊天轮(isChatStreaming=true)。用户点"停止"时 ChatPage 按
聊天轮走 scope=chat,abortAgentSession 在 agent 会话注册表里找不到会话
(任务不在其中),第一次点停止什么都停不掉。

修法(前端无法预知服务端启发式,用事件驱动重分类):
- 服务端:确认式生产任务分支的 tool:start 广播加 background: true 标记,
  聊天轮工具的 tool:start 不带。
- 前端 stream-events:收到带标记的 tool:start 且当前 isChatStreaming=true
  时,把本轮重分类为任务轮——isChatStreaming 归 false(停止按钮据此走
  scope=all 拿到任务控制器,用户可继续聊天),isStreaming 维持 true;
  挂起的 fetch 返回后仍由 sendMessage 的 finally 按"是否还有任务在跑"收尾,
  不会重复收尾或把任务流关掉。
- ToolExecution 加可选 background 字段并记到工具卡上;mergeTaskExecution
  从任务快照恢复的卡同样补该标记(快照来源必然是后台任务,终态快照替换
  整个 execution 时标记不能丢)。该标记供后续无 id 事件的回退路由使用。
2026-07-15 15:15:12 +08:00
Ma 8073aaa9f8 feat(studio): tag pipeline progress events with the task execution id
后台生产任务与聊天并行时,SSE 的 log / llm:progress / context:compression
事件只带 sessionId,前端只能把它们附加到"最近一张运行中的工具卡",导致
任务日志和进度串排进聊天轮的工具卡(反之亦然)。

服务端:/api/v1/agent 在构建 pipeline 之前生成确认式任务的 execution id,
经 buildPipelineConfig 新增的 executionIdForSSE 传给任务的 pipeline,该
pipeline 广播的三类进度事件都带上 executionId。按构建时打标而不是按
sessionId 查表打标:同会话并行聊天轮的 pipeline 是另一次请求单独构建的、
不带任务 id,聊天轮工具经 pipeline 广播的日志不会被误标成任务日志。

前端:log 与 llm:progress 处理器改为——事件带 executionId 时用
updateToolPartById 按 id 跨消息精确定位工具卡;不带时维持"最近一张运行中
的卡"回退(兼容聊天轮工具与旧版事件)。llm:progress 改为每个 executionId
一个节流器,任务与聊天并行时双方进度不会在"只保留最新事件"的节流器里
互相覆盖;带 id 但找不到卡的日志直接丢弃(任务快照重放会带回累积 logs),
不回退到最新卡。

测试:server 端验证任务运行期间广播的 log/llm:progress 带任务 execution
id、并行聊天轮与任务结束后的日志不带;store 端验证带 executionId 的
log/llm:progress 附加到更早的任务卡、不带的维持附加到最新运行中卡。
2026-07-15 12:23:20 +08:00
Ma 550bdf2a8a fix(core): align short-fiction length validation between confirmation and runner
根因:确认卡 payload 的 zod 校验(ShortRunActionPayloadSchema)对
charsPerChapter 只做 600-1200 的并集校验,而执行层(short-fiction-runner
的 boundedInteger)按语言分段校验:en 600-800 词/章、zh 900-1200 字/章。
两层不一致导致非法组合(如 en+1100)能通过确认卡校验并把任务跑起来,
用户要等 pipeline 中途才看到报错。

修法(两层收紧,错误尽早浮出):
1. action-envelope.ts:ShortRunActionPayloadSchema 加 superRefine——
   language 与 charsPerChapter 同时存在时按语言分段校验;language 缺省时
   维持 600-1200 并集(此时最终语言由会话默认决定,envelope 层无法预知)。
   分段区间直接引用 short-fiction 的常量(600/800、900/1200),与执行层
   共用同一组数字;新增 shortRunCharsPerChapterRange / Error 两个导出,
   错误文案 zh/en 双语并写明该语言的合法范围。
2. agent-tools.ts:createShortFictionRunTool 在启动 pipeline 之前,按最终
   语言(payload.language ?? 会话语言 ?? zh,与 runner 默认一致)校验
   charsPerChapter,越界立即抛出带合法范围的双语错误。
3. schema 即文档:propose_action 的 shortRun.charsPerChapter 与
   short_fiction_run 的 charsPerChapter description 补上"按语言分段、
   越界会在任务开始前被拒绝"的说明。

生效链路:propose_action 生成确认卡时(validateProposedActionPayload)、
Studio 确认请求进入时(normalizeActionPayload → 400)、工具启动前
(assertShortRunCharsPerChapter)三处都会在任务开跑前拦下非法组合。

测试:short-run-length-validation.test.ts——envelope 拒绝 en+1100 与
zh+650、接受 en+700 与 zh+1000、language 缺省时 900 合法;propose_action
对 en+1100 直接抛错;工具层会话 zh 默认 + payload en+1100 在启动前抛错且
不调用 runner;params 路径 en+1100 同样拦截;"en 未填字数 → runner 收到
undefined 并用自身 650 默认值"行为不回归。
2026-07-15 11:53:42 +08:00
Ma fc6babc335 fix(studio): persist the production instruction to the transcript at task start
根因:确认式生产任务(create_book/short_run/write_next 等)的用户指令,
要等任务完成/失败后才随助手工具消息一起传给 appendManualSessionMessages,
且只作为 request_started.input 落在 transcript 里——transcript 恢复逻辑
(deriveBookSessionFromTranscript)只把 role=user 的 message 事件还原成
用户气泡,从不读取 request_started.input。后果:
1. 任务运行期间刷新页面,GET session 的消息里没有这轮用户指令,用户气泡消失;
2. 任务完成后指令依然不会作为用户气泡恢复;
3. 并行聊天的消息先写入 transcript,重新加载后聊天排在生产指令前,顺序倒置。

修法:任务开始执行前(预留名额与快照检查之后、executeConfirmedProductionAction
之前)就把用户指令作为 role=user 的消息事件写进 transcript(时间戳为开始时刻);
成功与失败(ConfirmedActionExecutionError)两条完成路径改为只追加助手工具消息,
instruction 参数传空字符串,指令不会写第二遍。刷新场景下用户气泡来自 transcript、
运行中任务卡来自快照 mergeTaskExecution 按 execution id 合并,两者共存且不重复。

测试:server.test.ts 新增三条真实 transcript 往返用例(运行中刷新含指令气泡、
并行聊天轮后顺序按真实时间、失败路径指令只写一次),并把三条既有断言更新为
"开始预写指令 + 完成只补助手消息"的新契约;action.test.ts 新增前端恢复用例
(transcript 用户气泡 + 运行中任务卡合并不重复)。
2026-07-15 11:46:31 +08:00
Ma 790aace6b9 fix(studio): reserve the single-task slot atomically
根因:确认式生产任务的单任务检查是"await findActiveRunningTask 读快照 →
之后才 set AbortController"的 check-then-act 结构:两个并发确认请求都能在
对方注册 controller 之前通过检查,双任务同时启动、每会话唯一的任务快照
互相覆盖,前端丢失其中一个任务卡。

修法:
- server 闭包新增 reservedProductionSessions(sessionId 集合)。进入确认
  分支后、任何 await 之前同步预留名额:占位失败的请求直接按
  PRODUCTION_TASK_ALREADY_RUNNING 返回 409。
- 预留成功后再走既有的快照检查(防旧进程遗留的运行中快照等边界情况),
  检查移入 try 块,finally 里与 activeConfirmedTasks 一起释放,任何出错
  路径都不会遗留占位。
- 预留键固定为进入分支时的 sessionId:bookSession 之后可能因建书迁移被
  重新赋值,finally 释放的必须是当初预留的那个键。

测试:用 loadBookSession 做屏障让两个并发确认请求同时到达检查窗口——
恰好一个 409、一个正常执行,败者的任务没有启动,胜者的快照收敛为
completed。修复前该场景两个请求都返回 200、任务双启动。
2026-07-15 11:21:08 +08:00
Ma ea9bce50dd fix(studio): don't close a streaming chat when a terminal task snapshot lands
根因:stream-events.ts 的 task:snapshot 处理里,终态快照只要通过了
hasInFlightExecution 门槛就无条件置 stream:null、isStreaming:false 并关闭
连接。竞态场景:任务刚结束、消息里还留着 in-flight 任务卡,此刻用户发消息
建立了新连接且聊天轮正在流式——服务端在连接建立时重放终态快照,门槛通过,
正在跑的聊天流被关掉,本轮所有增量事件全部丢失。

修法:接受终态快照时照常 merge 收尾任务卡,但只有聊天轮不在流式
(!isChatStreaming,从会话 runtime 读取)时才关闭连接、置 stream:null 和
isStreaming:false;聊天轮流式中则保持连接和流式状态,等聊天轮请求结束时
由 sendMessage 的收尾逻辑处理。

测试:聊天轮流式中 + 消息有 in-flight 任务卡 + 收到该任务的终态快照 →
任务卡转 completed、连接未关、isStreaming/isChatStreaming 仍为 true;
聊天轮结束后连接正常关闭。
2026-07-15 11:13:44 +08:00