Skip to content

Latest commit

 

History

History
1344 lines (923 loc) · 256 KB

File metadata and controls

1344 lines (923 loc) · 256 KB

前后端与 Agent 工作台对接规范

版本:2026-08-11
适用范围:新任务、任务确认、/task/running、数据准备、建模方案、实验结果、论文编辑、最终成果。
事实来源:代码、JSON Schema、OpenAPI 与自动化测试;规划项单独标记为“待实现”。

1. 目标与非目标

本规范解决四个一致性问题:

  1. 首页输入如何经登录续接直接创建真实 Project/TaskRun(/confirm 作为直接访问入口共用同一流程);
  2. 后端 TaskRun 当前阶段如何恢复为可导航的页面语义;
  3. Agent 左栏的步骤、摘要和按钮如何与右侧页面保持一致;
  4. Artifact 如何进入现有实验/成果文件表,而不破坏视觉基线。

本批次不重做页面,不把所有演示正文一次性替换为真实结果,也不把 Agent 文本当成页面结构化数据。

2. 当前实现清单

层 已实现 事实文件
契约 ModelingWorkspaceView JSON Schema、Fixture、Python/TS 生成类型 packages/contracts/schemas/v1/modeling-workspace-view.schema.json
API GET /api/v1/task-runs/{run_id}/workspace backend/api/omm_api/routers/workspace.py
投影 节点→页面、页面状态、Agent 动作、Artifact 映射 backend/api/omm_api/workspace_view.py
Web API Project/TaskRun 创建、workspace 查询、真实 actions、错误信封、幂等键 apps/web/src/integration/modeling-workspace-api.ts
新任务控制器 草稿、直接创建、登录续接、幂等重试、demo 隔离 apps/web/src/integration/task-start-controller.ts、task-start-state.ts
Web 控制器 URL 恢复、DOM 渲染、SSE、清理、跨页身份、Artifact 文件行 apps/web/src/integration/modeling-workspace-controller.ts
页面接线 activateScreen() 末尾挂载;现有 DOM 增加语义槽位 apps/web/src/legacy/openmathmodel-ui.ts
开发代理 /api 默认 8000,可用 OMM_API_PROXY_TARGET 覆盖 apps/web/vite.config.ts

2.1 当前两个已接通切片

新任务切片覆盖:首页保存结构化草稿,发送时直接完成登录续接、创建 Project、以稳定幂等键创建 TaskRun,并携带 run_id/project_id 进入现有执行页。/confirm 不在首页发送链路上,保留为直接访问时的草稿复核入口,启动时执行同一套提交流程;无草稿确认页显式进入 demo,不创建后端资源。

工作台切片以已存在且归属当前用户的 run_id 为入口,覆盖:workspace 快照、六阶段状态、Agent 时间线/摘要/主操作、模型方案 Approval、SSE 通知后刷新、真实 Artifact 文件清单,以及 READY 文件下载。

附件切片覆盖:输入框拖拽/粘贴/点击三入口、浏览器内即时解析、二进制随任务创建上传为项目产物,以及服务端按需抽取的权威正文(见 2.3)。

当前尚未覆盖:五类页面详细正文契约、数据清洗确认/采用实验结果/论文完成交付等业务动作、可见暂停入口、独立 Worker 调度和完整 Skills 节点。当前详细正文继续使用页面模板,不应据此宣称 Agent 已生成真实指标、图表或论文。

2.2 新任务控制协议

首页草稿使用 openmathmodel.taskDraft.v1 保存:

description / task_type / selected_model
attachments[] = name / size / type / last_modified
             + format / parse_status / characters / excerpt / artifact_id?
project_id? / run_request_token?

草稿只带受控长度的 excerpt(单文件 4000 字、合计 24000 字):它落在 sessionStorage 里、还会随任务参数进数据库和事件流,几十万字的正文塞进去会直接把草稿写失败。完整正文留在服务端,由 2.3 的接口提供。

首页发送与确认页“开始任务”共用同一套提交流程,按固定顺序执行:

  1. 校验草稿(确认页先从 sessionStorage 恢复草稿;无草稿时显示 demo 说明);
  2. fetchMe(true) 确认 Cookie 身份;未登录或 401 时停在当前页打开现有登录模态,成功回调重新进入同一启动函数;
  3. POST /api/v1/projects,将返回的 proj_<32hex> 写回草稿;
  4. 等待浏览器内解析结束后逐个 POST /api/v1/projects/{id}/artifacts 上传附件二进制,把返回的 art_<32hex> 写回草稿;已有 artifact_id 的跳过,因此失败重试不会重复上传,任何一个附件上传失败都中止创建;
  5. POST /api/v1/task-runs,使用草稿内稳定 run_request_token 生成 Idempotency-Key;
  6. 任务参数包含 task_type、selected_model 与 attachment_metadata,并按实际结果写入 attachment_upload_state(none / uploaded / partial / metadata_only);
  7. 校验 Project/TaskRun 身份和归属后写入 active sessionStorage,再进入运行页。

附件必须赶在第 5 步之前落地:auto_start 的任务一创建 Agent 就开跑,晚到的附件进不了第一轮上下文。

创建请求失败时保留草稿与已写回的项目 ID:重新发送未修改的同一份草稿视为重试,沿用 project_id 与 run_request_token,不重复创建 Project/TaskRun;修改任务内容后发送视为新提交,两个标识重置。?demo=1 是显式演示身份,优先于历史 sessionStorage 中的 active run。

2.3 附件解析:浏览器即时预览 + 服务端权威结果

两侧各司其职,Agent 与论文环节一律以服务端结果为准:

浏览器(apps/web/src/attachments/) 服务端(omm_api/doc_text.py)
目的 拖进来立刻看到页数、字数与解析状态 供 Agent 消费的完整正文
触发 加入附件时 首次 GET /api/v1/artifacts/{id}/text,结果落 artifact_texts 长期复用
依赖 pdfjs-dist(PDF)+ fflate(OOXML/ODF/zip 解压) 标准库 zipfile/ElementTree + pypdf;旧版格式与 OCR 走可选依赖
上限 单文件 16MB、正文 20 万字、PDF 200 页 单文件 32MB、正文 40 万字、PDF 500 页

解析状态是五档,empty / unsupported / failed 同样是正常响应——调用方需要的是原因而不是错误码:

  • ready 完整抽出;partial 触顶截断;
  • empty 文件正常但没有文字(扫描版 PDF 落在这里);
  • unsupported 缺少可选依赖或格式不支持;failed 文件损坏或抽取出错。

浏览器解不动的(旧版 .doc/.xls/.ppt、RTF、图片 OCR、超限文件)在卡片上显示为“等待服务端解析”而不是失败。服务端的可选依赖与配置:legacy-docs 附加项提供 .doc/.xls/RTF;图片与扫描件 PDF 的识别走远程 OCR(讯飞星辰 MaaS 上的 PaddleOCR,OpenAI 兼容协议,配置 OMM_OCR_API_KEY 启用;本地 paddle 栈已于 2026-08-30 移除),输出逐页 Markdown(公式→LaTeX、表格→表格标记,engine="paddleocr-api"),扫描件 PDF 另需 pdf-ocr 附加项(pypdfium2 逐页渲染);未配置 key 时图片回落 ocr 附加项 + 系统 Tesseract。缺失时接口如实返回 unsupported/empty 并说明原因。

2.4 图片计数与单模态提醒(ADR-0010 批次一)

正文抽取只拿得到文字层,文档里的图对纯文本模型是不可见的。两侧解析因此都统计图片数并如实展示:

  • 服务端 Extraction.images / 接口字段 images(artifact_texts.images 缓存列):PDF 按页扫 /Resources//XObject 数 /Image 并按间接引用去重(Form 不递归,近似值);docx/pptx 数 word/media/*、ppt/media/*(精确);独立图片附件恒为 1;其余格式与计数失败为 null。计数绝不拖垮正文抽取。
  • 浏览器侧 ParseOutcome.images:PDF 为字节扫描 /Subtype /Image 的近似值(压缩对象流内的字典扫不到,卡片显示「约 N 张图」),OOXML 按 media 条目精确计数;随草稿进入任务参数(TaskAttachmentDraft.images)。
  • 单模态提醒:附件含图且生效模型判定为纯文本时,composer 附件托盘内显示提醒行(integration/model-modality.ts 按模型名模式分类;auto/endpoint-<id> 经 llm-config 解析主接口模型;未登录、未配置或模态 unknown 一律沉默——宁可漏报不可误报)。

后续批次(视觉解析后端、消息桥 image 内容块)见 ADR-0010。

2.5 对话附件:即席解析并入消息(ADR-0010 批次三)

任务页对话区输入框的附件走独立于 §2.2 的轻量链路——不建产物、不落库:对话历史本就只保存在页面内存,随消息提供的附件保持同样的隐私姿态。

  • 服务端 POST /api/v1/artifacts/parse(需登录):multipart 单文件,同步返回 AttachmentParseResult(与 ArtifactText 同一套 status/engine/characters/images/text 语义);超过大小上限返回 unsupported 并说明原因。抽取链路与产物正文完全相同,含可选 OCR/VL,图片与扫描件因此能转成 Markdown+LaTeX。
  • 发送消息时 attachments/conversation-context.ts 把附件折算成上下文块并入用户消息(单附件 8000 字、合计 20000 字预算):浏览器已抽到文字的直接用;解不动的(图片/扫描件/旧格式/自动解析被关)现场调即席解析,权威结果如实回写附件卡片。
  • agent-chat.ts 的 sendConversationTurn({ attachmentContext }) 只把上下文块并入请求内容,不进气泡展示;用户气泡下方以纸夹徽标展示附件名。发送成功后清空托盘,失败保留以便重试。
  • §2.4 的单模态提醒在对话框内同样生效。首页新建任务的附件仍走 §2.2 上传建产物链路,两条链路互不影响。
  • 任务附件同样进入对话:attachments/task-attachment-context.ts 读取工作台 artifacts 中无 producer_node 的 READY 产物,取 GET /artifacts/{id}/text 权威正文(与随消息附件同预算),按解析就绪进度逐轮并入运行页对话(含开场分析);发送成功才标记已注入,未就绪的附件在上下文中如实标注「仍在本机解析中」。图片/扫描件的解析全部发生在本机 API 进程(可选 VL),不出网。
  • 任务附件只属于对话绑定的运行(2026-09-07 修):sendConversationTurn 仅在当前归属是 run_… 时调用 collectTaskAttachmentContext(runId),run id 显式传入;首页对话(chat_…)与演示态不带任务附件。清单缓存按 run 分键,configureConversation 换绑时 resetTaskAttachmentContext()。此前该模块自己从标签页级 sessionStorage.openmathmodel.activeRunId 取身份,首页新开对话会把上一个任务页的附件正文并进第一条消息(见变更日志 2026-09-07 第二条)。

3. 工作台契约

3.1 顶层字段

字段 含义 消费者规则
run_id 运行身份 URL 与所有动作的主键
project_id/project_name 项目上下文 顶部项目名、跨页参数
goal 本次运行目标 后续任务详情与恢复
workflow_version 工作流定义版本 未知版本必须容忍新节点
run_status 生命周期轴 QUEUED/RUNNING/WAITING_APPROVAL/PAUSED/COMPLETED/FAILED/CANCELLED
active_node 领域阶段轴 与生命周期状态分开解释
active_page 当前页面语义 key 由后端唯一映射
suggested_route 当前阶段建议路由 仅作为导航目标;控制器不自动跳转,也不由此执行阶段动作
agent 左栏 ViewModel 文本只能按纯文本渲染
pages 六个页面状态 同时驱动时间线和右侧状态
artifacts 带状态的真实文件元数据 只有 READY 且存储引用完整时提供下载 URL;下载仍由 API 做所有权与哈希校验
pending_approval 当前待审批项 只在 PENDING 时出现
latest_event_sequence 已投影事件水位 SSE 从该 sequence 之后订阅
updated_at 快照更新时间 展示与诊断,不做并发控制

3.2 Agent 字段

{
  "state": "WAITING_APPROVAL",
  "title": "确认建模方案后继续实验",
  "summary": "确认后,Agent 将从当前检查点继续执行。",
  "current_step": "等待确认:确认建模方案后继续实验",
  "action": {
    "kind": "approve",
    "label": "确认并继续",
    "target_route": "/workspace/model-plan",
    "approval_id": "appr_<32hex>",
    "option_id": "approve"
  }
}

约束:

  • summary 是可显示纯文本,不允许内嵌 HTML。
  • action 是服务端允许动作,不是视觉按钮状态的猜测。
  • action 使用按 kind 判别的联合契约:navigate 必须有目标路由且没有审批字段;approve 必须有目标路由和审批 ID;supplement(题意解析因信息不足 E610 失败时代替 retry)必须有目标路由且没有审批字段;none 的路由、审批与 option 字段都必须为空。
  • 只有待审批项恰好存在一个非 reject 选项时,后端才自动填入 option_id;存在多个候选时保持为空,由接入真实 PlanProposal 的页面显式提交用户选择,禁止静默采用第一项。
  • 取消或不可操作终态使用 none,前端禁用按钮。

3.3 页面字段

页面状态枚举:

PENDING | RUNNING | WAITING_APPROVAL | PAUSED | SUCCEEDED | FAILED | CANCELLED

固定页面 key:

running | data | model | experiments | editor | complete

experiments 同时承接 EXPERIMENTING 与 VALIDATING。页面状态由当前节点、运行状态和每节点最新 attempt 共同推导,重试的旧失败 attempt 不覆盖新成功 attempt。

4. 节点、页面和内容映射

workflow node page key 路由 Agent 重点 右侧最终数据来源
CREATED running /task/running 任务已创建/排队 TaskRun + 输入
PROBLEM_ANALYSIS running /task/running 目标、约束、子问题 待扩展 ProblemFrame
DATA_PREPARATION data /workspace/data 数据质量与清洗 待实现 DatasetProfile
MODEL_PLANNING model /workspace/model-plan 方案比较、审批 待实现 PlanProposal
EXPERIMENTING experiments /workspace/experiments 运行与产物 待实现 ExperimentSummary
VALIDATING experiments /workspace/experiments 指标与稳健性 同上
PAPER_WRITING editor /workspace/paper-editor 论文生成与检查 待实现 DocumentDraft
COMPLETED complete /task/complete 交付完整性 待实现 DeliveryManifest

未知节点回退 running,Agent 摘要显示节点名但页面保持可用。增加节点时先改契约/投影测试,再改页面。

5. 首屏恢复协议

5.1 运行身份解析

优先级:

URL.searchParams.demo=1
  > URL.searchParams.run_id
  > sessionStorage.openmathmodel.activeRunId
  > demo 模式

合法格式为 run_<32hex>。实际分支如下:

  • URL 显式 demo=1:进入 demo 状态,不读取历史 active run;
  • URL 中存在且合法:使用 URL 值,并写入 sessionStorage.openmathmodel.activeRunId;
  • URL 没有 run_id 参数:读取同一标签页中格式合法的 sessionStorage 值;
  • URL 显式存在但不合法:不读取 sessionStorage,不请求 workspace API,进入 demo 状态;
  • 两处都没有合法值:不请求 workspace API,进入 demo 状态。

成功快照把 run_id/project_id 写回 sessionStorage,并装饰流程页链接。

5.2 启动时序

sequenceDiagram
  participant Page as 现有流程页面
  participant Controller as WorkspaceController
  participant API as FastAPI
  participant Events as SSE

  Page->>Controller: activateScreen(screen)
  Controller->>Controller: 解析 run_id / 创建 AbortController
  Controller->>API: GET /task-runs/{run_id}/workspace
  API-->>Controller: ModelingWorkspaceView
  Controller->>Page: 同步渲染项目/Agent/页面状态/Artifact
  Controller->>Events: GET /events?after=latest_event_sequence
  Events-->>Controller: run/step/approval/artifact event
  Controller->>Controller: 80ms 合并刷新
  Controller->>API: GET workspace
  API-->>Controller: 新快照
  Controller->>Page: 原位更新
Loading

5.3 清理

进入其他页面或 pagehide 时:

  • AbortController.abort();
  • EventSource.close();
  • 清除刷新定时器;
  • 移除 root 捕获监听器。

同一时间只允许一个工作台控制器实例。

5.4 错页语义

当前浏览器路由不会因 suggested_route 自动跳转。若当前 screen 与快照的 active_page 不同:

  1. 仍在当前页面渲染同一运行的项目名、Agent 时间线、摘要、页面状态和适用的 Artifact 清单;
  2. [data-agent-cta] 被转换为 navigate,目标为 suggested_route;
  3. 只有用户点击导航按钮后才带 run_id/project_id 进入目标页;
  4. 错页加载与导航本身都不调用 /actions,不审批、不暂停、不恢复、不重试,也不推进状态机。

5.5 合并工作台内的阶段导航(2026-08-12,ADR-0009)

五个阶段面板同存于一个合并工作台页面,阶段间跳转是软切换而非整页导航:

  • 切换由模板层 showWorkspaceStage 完成(面板显隐、顶栏返回键、history.pushState 别名 URL、标题更新);控制器经 omm:show-stage/omm:stage-shown 事件与其协作,SSE 连接与工作台快照跨切换存活;
  • 左栏时间线的非当前阶段行带 data-go:点击或键盘触发纯导航软切换,不调用 /actions;
  • agent.action 为 navigate 且目标是工作台路由时软切换;目标为 /task/running 或首页时整页导航;
  • 方案确认(approve 且 option_id ≠ reject)成功后自动软切换到实验面板——这是用户显式确认动作的延续,§5.4 禁止的仍是"加载时自动跳页";
  • popstate 在工作台路径之间换面板,路径离开工作台时整页导航兜底;
  • 顶部返回箭头统一指向任务执行页;成果面板例外指向首页。

5.6 对话轮:服务端托管生成与记录(2026-09-05,ADR-0016)

有归属(任务运行 run_… / 首页对话 chat_…)的对话不再走无状态的 /api/chat,而是服务端托管轮:一轮 = chat_turns 表的一条记录 + API 进程内的一个后台生成线程(omm_api/chat_turns.py ChatTurnHub)。页面只是观众——建轮即返回,经 SSE 附着直播,断了按序号续接;切任务、刷新、关标签页都不影响生成。

接口(routers/chat.py,全部要求登录):

方法 路径 说明
POST /api/chat/turns 建轮,202 {turn}。body = ChatRequest + scope_id / text / opening / attachments;同一归属已有 running 轮 → 409 CHAT_TURN_IN_PROGRESS;run_… 归属校验运行所有权
GET /api/chat/turns/{id}/events?after=N SSE:id: <seq> / data: {...,"seq"};事件与 /api/chat 同形(`meta → delta*/reasoning* → done
POST /api/chat/turns/{id}/stop 停止生成,半截正文保留(stopped)
GET / PATCH /api/chat/turns/{id} 轮视图 / 补写页面侧执行轨迹行 {trace}
PUT /api/chat/turns/{id}/feedback 回复右下角的赞 / 踩:`{feedback: "up"
GET /api/chat/scopes/{scope}/turns 该归属全部轮(按时间;running 的带半截 reply/reasoning 与 last_seq)
DELETE /api/chat/scopes/{scope} 删该归属全部轮,运行中的先停

轮视图字段:id, scope_id, status(running|completed|failed|stopped|interrupted), opening, text, attachments, reply, reasoning, meta{host,model,route,usage,elapsed_ms,error?}, error{code,message}?, trace, feedback(up|down|null), last_seq, live, persisted, created_at, updated_at, ended_at。

服务端行为:正文/思考每秒批量回写库;终态后事件缓冲保留 10 分钟供续接;stop 立即定格并关闭上游连接;进程启动时把遗留的 running 标成 interrupted(服务重启是唯一会中断生成的情形);用量记账只在正常完成时发生。

「保存任务历史」:开启 → 落库;关闭 → 只在内存托管(同样的直播窗口,重启即无);由开到关 → 服务端删该用户全部轮。删除项目级联删其运行的轮;删除首页对话前端先 DELETE /scopes/{chat}。

页面侧(integration/agent-chat.ts / chat-turns-api.ts):

  • sendConversationTurn 有归属时 startChatTurn → onTurnStarted(turn) → 附着 events;AbortSignal 触发服务端 stop(暂停键真的停生成);SSE 断线按 after=seq 重连(最多 5 次),失败则拉一次轮视图补齐正文;
  • 重进:modeling-workspace-controller 在首次 renderWorkspace 之前 hydrateConversation(run) 并广播 omm:conversation-restore{runId, goal, turns}(保证规划阶段的 omm:run-planning 不重复发起开场分析);页面层先渲染本机旧记录(只读兜底),再按时间重建服务端的轮,running 的那一轮原位续接直播(attachConversationTurn:半截先上屏、随后接实时增量、暂停键照常生效)。首页 restoreHomeChat 同理;
  • 模型看到的上下文(history)仍由页面维护并随每次发起携带:托管轮只托管「这一轮」的生成与记录;running 的与无正文的轮不进上下文(保住 user/assistant 严格交替);
  • 本机 localStorage 记录(tasks/conversation-log.ts)降级为只读兜底,不再写入;上一轮的「在途轮」(chatPending)与 pagehide 落定机制已移除。

没有归属的页面(演示态)仍走无状态 POST /api/chat,行为不变。非目标:不把整页导航改成软路由(界面基线不动)。

5.7 模型目录:厂商在售型号与单价由服务端同步(2026-09-06,ADR-0017)

设置中心「模型厂商」卡片的型号、「配置」一键填入的默认模型、「默认模型 ID」补全与「用量监控」的单价,不再写死在前端 / 后端代码里,而是服务端 omm_api/model_catalog.py ModelCatalog 从公共目录 models.dev(api.json,无需密钥)按 TTL(默认 6 h)同步、裁剪、落文件缓存(data/model-catalog.json)后提供:

方法 路径 说明
GET /api/llm/catalog 目录视图,永不阻塞在出网上:`source(catalog
POST /api/llm/catalog/refresh 立即同步后返回新视图;拉不到 502 MODEL_CATALOG_UNREACHABLE(旧数据保留);服务端关闭同步 409 MODEL_CATALOG_DISABLED;两次强制刷新至少间隔 60 s

providers[] 按厂商预设顺序(openai / anthropic / google / deepseek / qwen / kimi / zhipu / xai / ollama):id, label, logo, protocol, base_url, alt_hosts, subtitle, source(catalog|builtin|none), highlights[](最新三款正式版), models[]{id, name, release_date, reasoning, vision, context, status("" | beta | preview), input_usd, output_usd, alias}。裁剪规则与配置项见 ADR-0017。

页面侧:integration/model-catalog.ts(一次会话拉一次,refresh 后替换缓存)、integration/model-catalog-view.ts(纯函数:按厂商取亮点 / 型号、新鲜度文案、目录模态)。legacy/openmathmodel-ui.ts 打开设置时填卡片副标题与「型号目录同步于 … 来源 models.dev」状态行,新增「同步型号」按钮;「配置」默认模型取亮点首位;「默认模型 ID」补全先铺目录再合并接口自报清单。integration/llm-providers.ts 只保留品牌骨架(名字 / 图标 / 协议 / 地址 / 备用域名),不再含模型名。model-modality.ts 的模态判定先查目录 vision,未收录再回落命名规则。后端 usage.model_pricing 先按模型 ID 精确命中目录单价 × OMM_USD_CNY_RATE,未收录再走手写 PRICING 前缀表。

5.8 对话即控制面:任务页一句话真正驱动运行(2026-09-06,ADR-0018)

任务归属(run_…)的托管轮在调用模型之前先经运行控制步骤(omm_api/run_control.py run_control_step):读运行状态 → 按状态机算出合法动作集(与按钮同一套规则:FAILED→retry;WAITING_APPROVAL→approve / reject / cancel;PAUSED→resume / cancel;RUNNING / QUEUED→pause / cancel;COMPLETED 且修订未满 3 轮→revision;CANCELLED→无)→ 两级意图判定(本地词表规则先判;拿不准、≤200 字且带与合法动作相关的命令气味时,用接口池里最弱的模型做一次 10 s 限时 JSON 判定;判定异常 / 结果不在合法集内一律回落为普通对话,绝不误执行)→ 走既有路径执行(actions.execute_action / engine_glue.accept_revision / 运行备注)→ 产出 action 事件并把【当前运行状态】【本轮已执行的操作】注入系统提示词,再生成回复。

分级:retry(同事务把这句话落成 global 备注注入重跑阶段)/ resume / pause / approve(点名选项:判别性标签片段、「方案 B」、「第二个」、已知别名;不点名的「同意 / 确认 / 继续」≤12 字且非问句时选预选项——唯一 recommended 或唯一正向)/ reject / revision(已完成运行上点名阶段 + 修改动词本地直判,否则交判定;受理后照开 ADR-0013 修订门,点名阶段标 recommended 预选,下一句「确认」即重跑)直接执行;cancel 先发提案(status:"proposed"),只对紧接着的下一轮有效:「确认 / 好 / 取消吧」执行,「算了 / 不要 / 不取消」→ dismissed,其它文本作废提案按新意图处理。问句(为什么 / 吗 / ?…)不算命令;带命令词的客气问句(「能不能修复一下?」)按命令。无动作且运行非终态、非问句的文本照旧静默落成备注(不发事件)。chat_… 归属与开场轮不经控制步骤。

SSE / 轮视图新增:

{"type":"action","seq":1,"kind":"retry","status":"executed","stage":"EXPERIMENTING","stage_label":"实验运行","note_id":"note_…","message":"已重试「实验运行」阶段,并把你的要求作为备注注入该阶段的执行提示词"}
{"type":"action","seq":1,"kind":"cancel","status":"proposed","message":"要取消这个任务吗?…","confirm_hint":"回复「确认」或点下方按钮执行;回复其它内容则不取消"}
{"type":"action","seq":1,"kind":"approve","status":"executed","approval_id":"appr_…","approval_title":"确认建模方案","option_id":"adopt:plan_b","option_label":"…"}
{"type":"action","seq":1,"kind":"revision","status":"executed","round":1,"approval_id":"appr_…","note_id":"note_…","stage":"DATA_PREPARATION","stage_label":"数据准备"}
{"type":"action","seq":1,"kind":"cancel","status":"rejected","code":"INVALID_ACTION","message":"…"}

action 事件先于 meta / 正文;同时并入轮视图 meta.actions[] 随轮落库。status ∈ executed | proposed | rejected | dismissed。判定模型的用量按 source="route" 记账。

页面侧:chat-turns-api.ts RunControlAction / ChatMeta.actions;agent-chat.ts ChatHandlers.onAction(applyEvent 处理 action,重进续接时按视图 meta.actions 逐条重放;entryFromTurn 在轮没来得及补写 trace 时由回执重建轨迹行);integration/run-control-view.ts 纯函数把回执变成轨迹行(静态标题「已重试阶段」「已选定审批选项」「已受理修改要求」「等待你确认操作」「当前状态不允许该操作」「已放弃提案」…,阶段 / 选项名进后缀,服务端说明进详情);legacy/openmathmodel-ui.ts 的回复呈现器渲染回执行、proposed 行下挂「确认执行」按钮(等价于发一句「确认」)、executed 后广播 omm:run-reopened 让工作台控制器立即重拉快照并重接事件流;恢复态最后一轮仍留待确认提案时按钮照挂。页面不再在发送前 POST /notes,也撤下了「任务已结束,本条按问答处理 / 按这条要求继续修改」那条死路轨迹行;/notes 与 /revisions HTTP 端点保留(与控制面共用 record_run_note / accept_revision)。

动作生效后运行事件的落点(2026-09-07 补):executed 回执让控制器重接事件流后,重跑阶段的 run.node_changed / run.log / step.* 紧跟着到达,此刻对话末尾正是这条(往往还在生成中的)回复块。控制器 tailTraceHost 的三条规则:尾部是轨迹块 → 续写;尾部是对话回复块 → 在该回复块内部(正文与复制按钮之后)挂「收起执行步骤」折叠头 + .agent-stream.run-trace 继续写,不另起带署名的 Agent 块;尾部是用户消息等其它元素 → 另起一个与首条 Agent 消息同构的轨迹块。回复与它触发的执行步骤是同一轮的两半,页面上是同一条 Agent 消息;用户再发消息时后续事件按时间顺序落到新的尾部。配套:回复的复制按钮改为紧跟 .analysis-copy 插入(步骤常比回复先到);toggle-activity 折叠头优先折叠紧随其后的列表(回复块里同时有 .reply-trace 与 .run-trace)。回复口径随之分三档(run_control.reply_rules):本轮有 retry / resume / redo / approve / reject / revision 已执行 → 交接口径(两三句告知已做什么、运行接下来做什么、进度看执行步骤,不在对话里替运行算题 / 建模 / 写论文);有待确认提案 → 说明代价请用户确认;其余(无动作 / rejected / dismissed / pause / cancel)→ 原口径「告知操作再回答问题」。run.log{kind:"user_note"} 在活动流里改为叙述行(原先落进原始 JSON 兜底)。

非目标(见 ADR-0018 §5):不给对话模型开工具调用;不改引擎状态机(失败运行仍只能重试当前阶段,「失败运行选起点重做」留待下一刀)——已由 §5.9 / ADR-0019 修订。

5.9 任意状态下从选定阶段重做;判定以模型为主、规则只做快路径(2026-09-06,ADR-0019)

用户追问(上一刀之后):「你现在只是通过固定的关键词来识别判断吗?agent 自己没有相关的意图识别吗?我希望不管说什么,只要是需要执行的,都会出现真正的修改,而且这个修改显示的进度,也不一定要接着上一次的显示」。拍板:气味词只做第一层门槛,模型识别必须有;修改要求落成同一运行从选定阶段重做(上游成果保留、进度接原时间线);重做先发提案、确认后执行。

判定顺序(替换 §5.8 的两级判定):① 上一轮留有提案 → 只看确认 / 放弃词;② 本地规则命中且原文 ≤12 字 → 直判(省一次调用);③ 其余一律送判定模型(≤800 字;不再有气味词 / 问句门槛),提示词带运行状态、六阶段进度(已完成 / 失败于此 / 等待确认 / 暂停于此 / 正在执行 / 未开始)、失败原因、待确认选项、合法动作、最近三轮对话(用户 / 助手各截断)与这句原话;④ 模型答 none 或异常 → 回落规则结果(长句里的「继续想办法」仍是 retry);>800 字只落备注。判定的 redo 若点名尚未开始的阶段 → 视为 none(没有东西可重做,静默落备注,该阶段执行时读到);点名的正是 FAILED 的失败阶段 → 折成 retry(直接执行)。代价如实说明:任务页每句非短句对话前多一次弱模型调用(10 s 限时,source="route" 记账)。

redo 动作:合法状态 RUNNING / PAUSED / WAITING_APPROVAL(节点自提的闸门;ADR-0013 修订门上六个起点已是选项,不开 redo)/ FAILED;COMPLETED 仍走 revision;QUEUED / CANCELLED 不开。流程:提案 {"type":"action","kind":"redo","status":"proposed","stage":"MODEL_PLANNING","stage_label":"建模方案","text":"<用户原话>","message":"要从「建模方案」重做吗?…","confirm_hint":"…"} → 下一句「确认」/ 点按钮 → engine_glue.redo_run:原话落 global 备注 → 引擎 RUN_REDO 事件(reducer 关掉在途 RUNNING 步骤为 CANCELLED、清 review / failure / paused、状态回到目标阶段并 force_rerun、丢弃目标及下游旧产出)→ 投影:待审批门 CANCELLED(契约 resolution 留空,作废原因记内部 evidence.superseded_by)、在途步骤行 CANCELLED(detail「已被「从「X」重做」取代」)、清 failure_* / paused_from_status / ended_at、current_node 回目标阶段、状态 RUNNING → 回执 {"kind":"redo","status":"executed","stage","stage_label","note_id","message"},run.log 记 redo_requested。「不 / 算了」→ dismissed(「已放弃从「X」重做,运行状态不变。」)。

在途让位:RUNNING 下确认重做时,正在执行的节点不会被打断(同一运行同一时刻只有一个工作线程在推进、无协作式中断),其收尾写事件会撞 run_domain_events(run_id, seq) 唯一约束——推进器 WorkflowAdvancer.advance 只把这一种完整性错误当作让位契约(warning「in-flight step superseded by a control action」并丢弃结果),其它 IntegrityError 照常抛出;下一 tick 重放日志从目标阶段起跑,被取代的步骤不会再被 heal_interrupted 判成 executor lost。

页面侧只加文案:run-control-view.ts 新增 redo 的标题「已从阶段重做」与类别「从阶段重做」(提案 / 放弃行后缀带回到的阶段名),RunControlAction.text 字段;提案按钮沿用 status:"proposed" 通道,无 DOM 结构变化。

6. SSE 规则

当前事件类型:

run.created
run.status_changed
run.node_changed
run.log
step.started
step.succeeded
step.failed
approval.requested
approval.resolved
artifact.published
  • SSE id 等于 AgentEvent.sequence。
  • 显式 after 优先于 Last-Event-ID。
  • heartbeat 是注释 : ping,不进入历史。
  • 终态无增量时发送 stream.end;Web 最后刷新一次并关闭连接。
  • 终态之后运行再次离开终态(retry 把 FAILED 置回 RUNNING、修订受理把 COMPLETED 置回 WAITING_APPROVAL),Web 必须重新连接(after= 最后序号只接增量):控制器在任何一次快照刷新看到非终态而流已收尾时重连,不依赖是哪个动作触发的。
  • 事件只作为“快照可能变化”的通知,不直接把 payload 注入页面正文。

未来增加 stage.output.updated 时,payload 只带 stage/version/content_hash,完整对象仍从读接口获取。

7. 动作协议

7.1 审批

sequenceDiagram
  participant User as 用户
  participant Web as 现有方案页
  participant API as /actions
  participant Runner as 状态机

  User->>Web: 点击“确认并继续”
  Web->>Web: 禁用按钮,显示提交中
  Web->>API: approve + approval_id + option_id + Idempotency-Key
  API->>Runner: 解决审批并从检查点恢复
  API-->>Web: TaskRun
  Web->>API: GET workspace
  Runner-->>Web: approval.resolved / node_changed SSE
  Web->>API: GET workspace
  Web-->>User: 真实后端状态
Loading

失败时保持原页面,Agent 摘要区显示统一错误文案;不执行下一页跳转。

7.2 暂停、恢复和重试

底层 /actions、共享契约与 Web 控制器支持 pause、resume、retry,并复用幂等键。当前 workspace 投影只会在 PAUSED 时产生 resume、在 FAILED 时产生 retry(题意解析因信息不足 E610 失败时改为 supplement:前端把焦点送进同页对话输入框、不调用后端,补充内容由对话控制面改判为带题面补充的重试);运行中不会产生 pause,现有页面也没有可见暂停入口。因此暂停是接口能力,不是当前工作台已经交付的用户操作。运行状态确认前不改页面步骤为成功。navigate 不调用后端,但必须保留 run_id/project_id。

API 层 /actions 另支持 cancel(按状态机校验);当前 workspace 投影与页面主操作不暴露该动作,前端也不自行构造。

7.3 当前尚缺业务动作

数据清洗确认、采用实验结果、论文完成交付目前没有独立后端动作。生产接入前需决定:

  • 是否作为新的 Approval decision_type;
  • 是否属于具体阶段输出保存接口;
  • 或继续由自动工作流推进。

在决策前,前端不自创新 action 字符串。

8. DOM 渲染合同

8.1 左侧 Agent

数据 DOM 渲染规则
pages[] [data-agent-steps] 重建六行,状态映射图标与文字
agent.title/summary [data-agent-summary] 使用 textContent,防止注入
agent.action [data-agent-cta] 移除演示 data-go,绑定真实动作
project_name [data-bind="project-name"] 原位替换标题

完成步骤使用当前绿色完成样式;当前、审批、暂停和失败步骤使用现有 current 样式,不另造颜色体系。

六个流程页面当前都提供 Agent 时间线与摘要槽位;/task/running 和五个聚焦工作台页面也都提供 [data-agent-cta]。主操作必须经过 actionForScreen():错页只能得到 navigate,同页才允许消费后端投影的 approve/resume/retry 等动作。

8.2 右侧页面

工作台根节点与右侧阶段容器分别写入:

[data-modeling-shell] data-active-page=<active_page>
[data-modeling-shell] data-stage-status=<viewed_page.status>
.focused-stage-pane/.modeling-stage-pane data-workspace-page=<current_screen>
.focused-stage-pane/.modeling-stage-pane data-stage-status=<viewed_page.status>

因此后端活动阶段与用户当前查看页面不会混为一个属性:错页时左侧仍显示全局真实进度,右侧仍保留当前模板并呈现该页面自己的阶段状态。页签仍由原有 bindScreen() 切换;控制器不重新创建页签或替换正文结构。

这里的“右侧渲染”目前仅指 data-workspace-page、data-stage-status 和适用的 Artifact 文件行。数据质量指标、角色化方案、实验图表、论文正文和成果摘要尚未由 ModelingWorkspaceView 提供。它们必须等待五类独立正文契约,不可用阶段状态属性冒充真实内容接入。

8.3 Artifact 文件行

真实文件行沿用 .deliverable:

  • data-artifact-id 标识真实 Artifact;
  • 名称来自投影 name;
  • 类型来自 kind;
  • 大小按 B/KB/MB/GB 格式化;
  • 下载按钮使用 data-artifact-download。

状态行为:

  • READY 且 download_url 存在:显示“可下载”,按钮可用;
  • PENDING、STALE、DELETED:显示对应真实状态,下载按钮禁用且不设置 data-artifact-download;
  • workspace API 仅在 Artifact 为 READY 且 uri/sha256 完整时返回 download_url。

面板分组:

页面面板 kind
实验/结果图表 figure
实验/结果表 table, dataset
实验/运行日志 log
实验/模型代码 code, model
完成/最终成果 全部
完成/论文文件 paper, report
完成/数据与代码 dataset, code, model
完成/交付记录 log, other

没有匹配文件时显示“该阶段尚未发布产物”,不回退到演示文件。底部“导出文件清单”导出名称、类型、状态、大小与下载地址的 TXT 清单;它不是 ZIP、归档包或多文件合并下载。

8.4 集成状态标记

控制器在根节点上维护机器可读的接入状态,浏览器测试应以这些属性为断言锚点:

  • [data-modeling-shell] 的 data-integration-state:loading → ready / error / demo 四值生命周期;快照成功时同时写入 data-workspace-source="api"。
  • [data-task-start-root] 的 data-task-start-state:draft / ready / loading / auth-required / created / error / demo;data-task-start-source 标记 local / draft / demo 来源。

9. API 投影算法

build_modeling_workspace_view() 每次请求:

  1. 已由路由完成运行归属校验;
  2. 查询全部 StepRun,按 node 选最新 attempt;
  3. 查询 run Artifact,并用 producer step 反查 producer node;
  4. 查询最新 PENDING Approval;
  5. 查询 AgentEvent 最大 sequence;
  6. 由当前节点和生命周期计算六页状态;
  7. 生成 Agent 摘要与允许动作;
  8. 由 Pydantic 生成类型校验响应,再交给 FastAPI response_model。

该接口不新增表,不改变状态机,不持久化派生页面状态。

10. 安全、错误与兼容

  • workspace 与 actions 都使用同源 Cookie 身份。
  • 非本人运行与不存在运行统一 404。
  • Agent 文案按纯文本渲染。
  • 下载仍走现有 Artifact endpoint,服务端重新计算 SHA-256。
  • 未知 workflow_version/current_node 不使前端崩溃。
  • 401/404/409/网络错误保留原页面,并在 Agent 区原位呈现。
  • workspace 返回 404(运行不存在或非本人)时,控制器同时清除 sessionStorage 中的活动 run_id/project_id:当前页保留错误提示,其余流程页回到演示态自愈,避免本地数据库重置后整个标签页持续报错。
  • URL 显式提供非法 run_id,或 URL 未提供且 sessionStorage 也没有合法值时,不发 workspace 请求,支持 UI 独立预览与视觉回归。

11. 开发环境

完整联调从仓库根启动:

npm run dev

统一入口启动或复用 127.0.0.1:8000 的 API;健康检查必须返回成功状态码(2xx)、JSON 且 status: ok,随后才启动 Web。登录、账户设置与带 run_id 的工作台都需要 API;npm run dev:web 只启动 Vite,主要用于静态页面预览或与手工启动的 API 配合。Vite 参数通过 npm run dev -- --host <HOST> --port <PORT> 传入。这样拉起的 API 不带热重载:改了后端或智能体代码要把 API 整个停掉再启动。复用已运行的 API 时会打印它的代码版本与启动时间(/api/system 的 code),它启动之后源码又改过就醒目提醒;运行中每一步也会记下执行代码的身份(见 2026-10-03 记录)。

默认 Vite 代理:

/api → http://127.0.0.1:8000

隔离联调可设置:

$env:OMM_API_PROXY_TARGET='http://127.0.0.1:8010'
npm run dev --workspace @openmathmodel/web -- --port 5175

该变量只影响开发/预览代理,不进入浏览器构建产物。

统一入口只会自动启动本机 loopback HTTP API;OMM_API_PROXY_TARGET 指向远程或非 loopback 地址时,目标必须已经健康,脚本不会在本机冒充该服务。

开发/预览服务器还内置两个中间件,均不进入生产构建产物:

  • GET /api/account/me:无会话 Cookie 或 API 不可达时由 Vite 直接降级返回 401 访客态 JSON,不产生代理错误。单独运行 npm run dev:web 时页面因此显示访客而不是报错;调试登录问题时先确认 API 是否在运行。
  • /paper-files/<年份>/<题组>/<文件>.pdf:优先读取本地 datasets/raw/sources/github/zhanwen-MathModel/papers 缓存,未命中再按固定 revision 回源 GitHub raw。论文阅读页依赖该路由;脱离 Vite 的静态部署需要另行提供同路径静态服务。

端到端诊断顺序固定为:

GET /api/health
  → 登录并保留 Cookie
  → POST /api/v1/projects
  → POST /api/v1/task-runs(记录返回的 run_id)
  → GET /api/v1/task-runs/{run_id}/workspace
  → 在现有页面 URL 携带 run_id/project_id

可直接复制的 PowerShell 请求见根 README 的“用真实 run_id 验证工作台”。若健康检查失败,先修复 API 启动;Vite 代理错误不作为页面渲染问题处理。

12. 验收要求

层 用例 预期
Schema valid fixture JSON Schema 通过
生成物 TS/Python check 与 Schema 完全同步
API 待审批 workspace active=model,action=approve
API 完成 workspace 六页完成,含实验/论文 Artifact
API 跨账户 404
Web 首页发送真实启动 登录续接后 Project/TaskRun 只创建一次,直接进入执行页且 URL 携带合法 run_id/project_id;创建过程中发送键禁用
Web 首页未登录发送 停在首页并打开现有登录模态;登录成功后续接同一次创建
Web 失败后重复发送 未修改内容时沿用 project_id 与幂等 token,不重复创建;修改内容后视为新提交
Web 确认页直接访问 草稿、任务类型、模型与附件元数据恢复;“开始任务”走同一提交流程且重试不重复创建
Web 无草稿确认页 显式 demo,不请求创建接口、不复用旧 active run
Web workspace 404 当前页原位报错并清除活动运行;其余流程页回到演示态
Web URL 与 sessionStorage 都无合法 run_id demo 状态,无 API 请求错误
Web URL 显式非法 run_id 不回退 sessionStorage,保持 demo
Web 待审批 run 六阶段真实时间线,按钮可提交
Web 点击审批 后端动作成功,SSE/快照刷新
Web 错页打开 run 只提供导航;不自动跳页、不请求 /actions、不改变运行状态
Web 跨页 查询参数保留
Web 工作台内切换阶段 无整页重载;URL、标题与投影同步更新
Web 浏览器后退/前进 工作台面板间穿梭不重载;离开工作台整页导航
Web 方案确认成功 自动软切换至实验面板
Web 论文页 只保留大纲、工具栏、正文
Web 完成页 真实 Artifact 或空状态;仅 READY 可下载
Web 导出文件清单 生成 TXT 清单,不生成压缩包
Build typecheck/check/build 全部退出码 0
API 删除 / 清扫链路(PostgreSQL) 凡改动 privacy.py 的删除逻辑、或新增带外键指向 task_runs / projects / artifacts 的表,本地必须用 OMM_TEST_DATABASE_URL=postgresql+psycopg://openmathmodel:openmathmodel@127.0.0.1:5433/openmathmodel_test 指向 tools/pg-dev.ps1 的测试库再跑一遍相关用例;并在用例里逐表数行断言从属行已清空

12.1 为什么删除链路不能只看 SQLite 绿

测试夹具默认用 SQLite,而 SQLite 默认不检查外键、时间列取回是 naive。两类只有 PostgreSQL 才会暴露的缺陷因此在本地全绿、到线上才炸:

  • 外键漏删(2026-09-02,B8):stage_outputs(0017)、run_notes(0018)、paper_exports(0016)三张表加入后,privacy._delete_runs 一直没有连带删除;SQLite 上 DELETE /projects 照样 204,PostgreSQL 上则 ForeignKeyViolation——侧栏「删除任务」与「任务保留」清扫对任何有阶段产出 / 追问备注 / 论文导出的运行一律 500。修复后新增的用例除了断言 204,还逐表数行,让 SQLite 也能抓到漏删;
  • 时区比较(此前 CI api-postgres 作业翻车):ended_at 在 SQLite 取回 naive、PostgreSQL 取回 aware,裸比较在 PG 抛 TypeError,见 privacy._expired_run_ids 的 as_utc。

CI 的 api-postgres 作业会在真实 PostgreSQL 上跑全量 API 测试,但它只能拦住有用例覆盖的路径;新增带外键的表时,删除链路的用例要一并补上,否则 CI 也是假绿。

13. 当前已验证证据

记录日期:2026-08-11;2026-08-12 增量见本节末尾。标题不携带日期,保证跨文档锚点稳定。

以下命令/测试已经实际执行并通过:

证据 结果
npm run check --workspace @openmathmodel/contracts 通过
npm run check --workspace @openmathmodel/web TypeScript 与 ESLint 通过
npm run build --workspace @openmathmodel/web 生产构建通过,Vite 转换 88 个模块
python packages/contracts/validate.py 8 个 Schema、28 个 Fixture 通过
python packages/contracts/check_compat.py 兼容性检查通过
python packages/contracts/scripts/export_openapi.py --check OpenAPI 基线通过;公共 Timestamp component 保持兼容
pytest backend/api/tests -q 66 个用例通过;其中 workspace 覆盖排队、待审批、动作字段不变量、审批选项歧义、完成聚合、非 READY 与存储可读性、跨项目异常关联、跨账户与 OpenAPI 兼容(2026-08-12 增至 75 个,见下)
node --test apps/web/src/integration/task-start-state.test.mjs 4 个新任务状态纯函数用例通过:草稿解析、输入归一化、项目名派生、运行 URL
新任务相关 API 回归 Project、TaskRun 幂等创建和 workspace 投影共 14 个相关用例通过
关键流程人工浏览器验收 首页→确认、草稿恢复、访客登录拦截、无草稿 demo 隔离,以及 /task/running、方案审批、跨页身份、论文页、完成页与真实 Artifact 行通过;控制台无错误

这些证据验证了契约、类型、生产构建、任务启动状态函数、workspace API 核心投影及关键人工流程。真实登录浏览器中的最终 Project/TaskRun 写入尚未在本轮 IAB 会话落库;两个创建接口、幂等语义和 workspace 投影已有后端回归覆盖。Web 控制器 DOM、SSE 重连、错页导航、其余 URL/sessionStorage 分支与非 READY 下载禁用态仍缺自动化浏览器覆盖,全部 14 条路由的自动视觉回归也尚待补齐。

2026-08-12 增量

行为与代码变化:

  • 首页发送链路由「跳 /confirm」改为「直接创建并进入执行页」;/confirm 退出首页发送链路,保留为直接访问的草稿复核入口,与首页共用同一套提交流程;
  • 修复重试幂等:失败后重新发送未修改的草稿(首页)或再次点击“开始任务”(确认页)沿用已写回的 project_id 与 run_request_token,不再重复创建;任一内容变化即重置两个标识,避免旧幂等键携带新内容触发 409;
  • 修复 workspace 404 自愈:清除 sessionStorage 活动运行,当前页原位报错、其余流程页回到演示态;
  • 被中止的动作请求不再向 Agent 摘要区渲染错误文案;
  • agent-event.schema.json 描述由“PostgreSQL 事件表”修正为“数据库事件表(当前默认 SQLite,目标部署 PostgreSQL)”,并重新生成 TypeScript/Python 模型与 OpenAPI 基线。

当日重跑并通过:契约 check、Web check、Web 生产构建(88 个模块)、validate.py(8 Schema/28 Fixture)、check_compat.py、export_openapi.py --check、pytest backend/api/tests(66 个用例)、node --test 新任务状态用例(4 个)。

尚未重新验收:第 12 节中“首页发送真实启动”“首页未登录发送”“失败后重复发送”“确认页直接访问”“workspace 404”等浏览器用例需要在真实浏览器中重新执行;上表 2026-08-11 的“首页→确认”人工验收记录对应旧链路,不再代表当前行为。

2026-08-12 账户头像

设置中心「账户与安全 → 编辑资料」支持更换和移除头像:

  • 后端新增 POST/DELETE/GET /api/account/avatar;内容按 sha256 存入独立头像目录,users 表只存引用;格式按文件魔数判定(PNG/JPEG/WebP/GIF),读取按当前会话返回本人头像并带 nosniff;user.avatar_url 携带摘要查询串做缓存失效。
  • 前端交互在账户与安全面板的头像本身:鼠标悬停(或键盘聚焦)时头像浮出相机图标,点击直接选图,本地居中裁剪压缩到 256×256 后立即上传并就地预览,完成后以服务端快照收尾;已设置头像时身份区提供“移除头像”。编辑资料弹窗保持原有的名称/邮箱/密码三项,不承载头像。侧栏、设置账户卡与安全面板三处头像统一由 avatar_url 渲染,未设置时回落姓名首字母。
  • SQLite 开发库在启动时补齐模型新增可空列,已有 dev.db 无需删库(详见 API README)。

当日执行并通过:pytest backend/api/tests(75 个用例,含 8 个头像用例与 1 个开发库补列用例)、npm run check --workspace @openmathmodel/web、npm run check --workspace @openmathmodel/contracts、npm run build --workspace @openmathmodel/web、export_openapi.py --check(基线已随新路由刷新)、对真实 uvicorn 实例的 12 项 HTTP 链路检查(注册→上传→读取→越权→移除,含伪装成 PNG 的 SVG 被拒)。

尚未验收:换头像的浏览器视觉走查(悬停遮罩、三处头像同步、暗色主题、侧栏折叠态)需在真实浏览器中执行并留存截图。

2026-08-12 高级设置:做实两项、如实标注四项

设置中心「高级设置 · 网络与运行」原有六个控件全部只有外观。本轮按可行性分级处理:

  • 做实「请求超时」:新增 apps/web/src/preferences/network-preferences.ts 读取器(默认 120 秒,夹紧到 5–600 秒),工作台客户端与账户客户端的全部 JSON 请求统一挂 AbortSignal.timeout,超时报出可操作的中文提示;SSE 长连接与附件上传刻意豁免。
  • 做实「最大并发任务」:上限存服务端而非浏览器——users 表新增可空列 max_concurrent_runs(迁移 0009,NULL=默认 3),新增 GET/PUT /api/account/preferences;创建任务时后端只统计排队与执行中的运行(等待审批/已暂停不占位),超限返回 409 CONCURRENCY_LIMIT。前端打开设置面板用服务端值回填显示,保存时异步推送,未登录时提示登录后生效。
  • 如实标注其余四项:代理两项注明"将随模型服务接入后生效"(后端目前没有任何出站调用);下载目录与临时文件目录是桌面端形态,网页版禁用并注明由浏览器/部署配置管理。

当日执行并通过:pytest backend/api/tests(97 个用例,含 9 个偏好与并发闸门用例)、export_openapi.py --check(基线随新路由刷新)、npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web、node --test(34 项,含超时读取器与并发解析 7 项)。

尚未验收:高级设置面板的浏览器走查(提示文案排版、禁用态样式、并发上限回填与 409 提示、暗色主题)。

2026-08-12 界面英文与桌面通知

设置中心的「界面语言」与「桌面通知」两个此前只有外观的控件已经真正生效:

  • 界面语言新增 apps/web/src/i18n/(词典 + DOM 适配层 + locale 存取),策略与边界见 ADR-0008。切换即时生效、未保存关闭即还原、启动前应用、<html lang> 同步;真实数据与 contenteditable 正文不参与翻译。
  • 桌面通知新增 apps/web/src/notifications/desktop-notifications.ts,接入工作台快照的状态变化:待确认、完成、失败各提醒一次,首屏不提醒,用户正注视页面时不打扰;权限在开关的点击手势里申请,被拒绝时开关自动拨回。

当日执行并通过:node --test apps/web/src/i18n/en-US.test.mjs(5 项词典门禁:非空、键已修剪、无原样返回、无残留中文、无重复键)、npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web。词典对已抽取的 1232 条界面文案覆盖 1202 条(97.6%);未覆盖的 30 条是源码注释与由变量拼接的提示词片段,其中工作台运行元信息一行已改为逐段 t()。

尚未验收:中英切换与桌面通知的浏览器走查(见 Web 页面基线 §10.3)。方法库与知识库的中文内容数据按 ADR-0008 保持原语言,不属于漏译。

2026-08-12 外观与显示:删两项、做实三项

设置中心「外观与显示」原有六个控件,其中只有主题真正生效。本轮按产品判断做了取舍:

  • 删除「界面密度」与「代码字体」。前者是持续税——每新增页面都要维护三档间距;后者是从编辑器类产品照搬的选项,在任务型工具里几乎无人使用。分区标题相应从「显示密度」改为「正文与可读性」,侧栏副标题也同步修正,避免文案继续宣传已不存在的功能。
  • 做实「正文字号」「减少动态效果」「增强文字对比度」。新增 apps/web/src/accessibility.css(表现)与 apps/web/src/preferences/display-preferences.ts(状态),在 main.tsx 中最后引入;两张受保护样式表零改动,仅被叠加覆盖。状态以 --omm-text-scale、data-reduce-motion="on"、data-contrast="high" 写在 <html>,与主题 data-theme 同一机制,并同样支持即时预览、未保存还原、启动应用。

三项的实现边界:

  • 减少动效无条件跟随系统 prefers-reduced-motion,应用内开关只加强、不取消系统偏好;过渡时长压到 0.001ms 而非 none,transitionend/animationend 仍会触发,依赖这些事件的交互不受影响。
  • 正文字号作用于继承 body 的阅读文本,外加 Agent 摘要与论文编辑器正文两处硬编码 px 的主要阅读面;标题、标签、徽章保持设计尺寸(与 Slack、Notion 的字号设置同一取向)。滑块基准由 15 修正为 14,与样式表实际基准一致。
  • 增强对比度修正的是真实缺陷:默认调色板中 --faint(#a1a19d) 与白底对比度约 2.6:1、--muted(#777773) 约 4.5:1,均未达 WCAG AA 对正文的 4.5:1 要求。开关打开后重定义 --ink/--muted/--faint/--line,浅色与暗色各一套。

当日执行并通过:node --test apps/web/src/preferences/display-preferences.test.mjs(5 项取值规范化用例,其中一项当场抓出空字符串被 Number("") 变成 0 再夹成最小字号 13px 的缺陷并已修复)、node --test apps/web/src/i18n/en-US.test.mjs(5 项)、npm run check 与 npm run build --workspace @openmathmodel/web,并确认三项的 CSS 均已进入构建产物。

尚未验收:三项的浏览器走查,以及暗色主题下高对比度的观感。

补记:2026-08-12 下午曾把「正文字号」改为基于 #root zoom 的「界面缩放」,当日按产品决定回滚,恢复上述正文字号方案;--omm-text-scale、设置文案与 JS 定位逻辑均已还原,无残留。

2026-08-12 任务与文件:做实三个开关

设置中心「任务与文件」的三项此前只是落盘的布尔值,本轮全部接上真实行为。读取器集中在 apps/web/src/preferences/task-preferences.ts,与面板初始状态一致(未保存过设置视为开启),每次使用时重新读取,改动后无需刷新即可生效。

  • 自动保存任务(autoSave):apps/web/src/tasks/task-autosave.ts,activateScreen 末尾挂载。工作台六屏(running/data/model/experiments/editor/complete)每 30 秒落盘两类现场:论文编辑器正文(localStorage,按 project_id 区分,跨会话),工作台输入框未发送的对话草稿(sessionStorage,按屏幕与 run_id 区分);pagehide 时补一次落盘,回到页面时自动恢复,编辑器顶栏芯片显示「已自动保存 HH:MM」。首页输入框不归它管——新任务草稿在 task-start-controller 里逐键即时保存。
  • 启动时恢复上次任务(restoreSession):workspace 每次成功渲染真实运行时把 run_id/project_id 写入 openmathmodel.lastTask.v1(tasks/last-task-record.ts),运行 404 时清除;main.tsx 在本标签页会话的首次加载落在 / 时读取记录并 location.replace 到运行工作台(tasks/restore-last-task.ts),已跳转则跳过首页渲染。同会话内再回首页不拦截(sessionStorage 一次性标记)。
  • 自动解析上传文件(autoOpenFiles):attachments/store.ts 的解析入口按开关短路——关闭时不在浏览器里抽取内容,附件直接置为「等待服务端解析」,settled()、上传与草稿流程不受影响,服务端解析后仍出权威结果。

当日执行并通过:node --test apps/web/src/tasks/last-task-record.test.mjs(3 项记录解析用例)、npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web。

尚未验收(需浏览器):编辑论文正文离开再回来是否恢复并显示保存时间;新标签页打开 / 是否直达最近任务、点击 Logo 回首页不再被拦;关闭自动解析后附件卡片是否显示「等待服务端解析」且发送流程正常。

2026-08-12 下线主题切换

深色主题长期只覆盖 styles.css(约 320 条规则),workflow-refresh.css 的聚焦工作台五个页面从未有深色规则,夜间模式实际不可用。当日下午曾尝试以补全皮肤(theme-dark.css,约 700 行)修复,因整体配色质量不达标、且每个新组件都要持续维护深色对应,按产品决定改为整体下线主题切换功能,界面固定浅色:

  • 设置中心「外观与显示」移除「界面主题」分区,侧栏副标题同步改为「正文字号与可读性」;正文字号、减少动效、增强对比度三项保留不变。
  • 移除 applyTheme/normalizeTheme/savedTheme 与主题的保存/回填/即时预览/恢复默认逻辑;openmathmodelSettings 里历史残留的 theme 键读取时静默跳过。theme-dark.css 已删除。
  • 保留而未清理的部分:styles.css 与 attachments.css 中既有的 html[data-theme="dark"] 规则成为不可达死代码(受保护基线,未做大规模删除);renderCharts 的按主题取色分支保留(data-theme 永不再置位,恒走浅色)。如后续重启深色主题,从这两处加上 git 历史里的 theme-dark.css 可恢复。(已于 2026-09-18 全部删除,见下文同名条目。)
  • initInterfaceLocale 之外不再有主题相关启动逻辑;增强对比度的暗色变体选择器(accessibility.css)同样不可达,保留。

当日执行并通过:node --test(en-US 词典 5 项、显示偏好 5 项、任务记录 3 项)、npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web。

尚未验收(需浏览器):设置中心外观分区只剩可读性三项、保存/恢复默认不再改动主题、历史保存过深色偏好的浏览器打开后界面为浅色。

2026-08-12 设置中心「诊断」做实

高级设置的诊断分区此前三个按钮全是假动作(定时器伪装诊断、toast 假装打开目录、剪贴板写死字符串)。本轮全部接真:

  • 后端新增 GET /api/system(ops,免登录,同 /api/health):返回服务名、版本、Python 版本、数据库方言名、runner 开关与服务器时间;刻意不暴露连接串、路径与凭据(test_system_endpoint.py 断言序列化结果不含 :// 与文件路径)。OpenAPI 基线已重新导出并通过 --check。
  • 前端新增 apps/web/src/diagnostics/system-diagnostics.ts:并发探测 /api/health(连通性与延迟)、/api/system(后端信息,404 归为提示以兼容旧后端)、/api/account/me(登录态,401 视为接口正常),5 秒超时;报告文本汇总前端环境(UA/语言/视口/在线状态)与后端信息。
  • 「运行网络诊断」把三项结果渲染为分区内的状态行(通过/提示/异常三色图标);「打开日志目录」浏览器内无真实语义,改为「导出诊断报告」(下载 txt);「复制系统信息」复制同一份报告,剪贴板不可用时提示改用导出。三个按钮执行期间禁用并显示进行中文案。
  • 图标排版修正:.diagnostic-actions button 改 inline-flex 居中 + 7px 间距,图标统一 15px,不再随文字基线漂移。
  • 英文词典同步:移除“打开日志目录”等失效词条,新增诊断结果与提示语翻译。

当日执行并通过:pytest backend/api/tests -q(98 个用例)、export_openapi.py --check、node --test(en-US 词典 5 项)、npm run check 与 npm run build --workspace @openmathmodel/web;对运行中的后端实测 /api/system 返回 200 与预期字段。

尚未验收(需浏览器):三个按钮的交互与状态行视觉、导出的 txt 内容、未登录/后端停机两种场景下的诊断结论展示。

2026-08-13 自定义 API 全链路做实(对话回复 + 任务执行)

设置中心「自定义 API」此前整页是摆设:表单只落 localStorage、「测试连接」是定时器假动作、已保存接口是写死的演示行、三个开关无消费方。本轮全部接真,对话回复与任务执行统一按该配置在服务端出网调用模型(密钥不下发页面、无浏览器跨域问题):

  • 后端新增 users.llm_config(迁移 0010,JSON 可空列兼容 SQLite 补列):已保存接口列表(名称/协议/Base URL/密钥/模型/组织/自定义请求头/路径前缀)、主接口与三个行为开关;GET/PUT /api/account/llm-config 仅本人可读写。
  • 新增 omm_api/llm.py:接口协议下拉的五个选项全部可用——OpenAI Compatible / Ollama / 自定义 REST 走 Chat Completions 形状(Base URL 为裸域名时自动补 /v1/chat/completions,已带路径则原样拼接,显式路径前缀最优先),Anthropic Messages(x-api-key + anthropic-version)、Gemini generateContent(key 走查询串)各自映射;流式 SSE 逐协议解析、用量归一并输出结构化用量日志(「记录接口用量」)。「允许使用第三方中转站」关闭时仅放行官方域名与本机地址(403 PROXY_DISABLED);「失败时自动切换备用接口」仅在超时、网络层失败或 HTTP 429 时按保存顺序切换,模型侧 4xx/5xx 属配置问题不重试;流式一旦开始输出不再回退,避免内容重复。httpx 升为运行时依赖;测试经 _transport_factory 注入 MockTransport,全程不出网。
  • 新增 POST /api/chat(需登录):无状态代理,对话历史随请求携带、服务端不落库;按「流式输出」开关走 SSE(meta/delta/done/error 事件)或一次性 JSON。POST /api/llm/test:用表单当前值做最小补全,返回时延、实际模型与回复摘录,验证地址/密钥/模型 ID 全链路。
  • 任务执行换脑(engine_glue):按 run → project.owner → users.llm_config 装配节点,配置可用时「问题分析」「建模方案」两个阶段走 omm-agent-skills 真实 LLM 节点(goal→problem_statement 适配;JSON Schema 校验 + 一次修复重试;方案 A/B 产出后停在审批),未配置或提示词缺失时整链回落 sim-0.1;其余四阶段仍为模拟节点,待后续批次替换。omm-agent-skills 的 requires-python 放宽至 3.10(与 core 同理:纯标准库 + 惰性注解,backend venv 当前为 3.10)。
  • 前端:对话页发送后的假回复(650ms 定时器固定文案)改为真实流式渲染(integration/agent-chat.ts),完成后标注实际域名、模型与是否切换备用(第三方中转站按开关文案显示实际域名),未配置/未登录时提供「前往设置中心配置模型接口」入口;设置保存时同步 llm-config(更新主接口或创建首个;example.com 示例占位视为未配置,避免把演示值当真实接口);「测试连接」「保存为新接口」接真;已保存接口列表从服务端渲染,菜单支持设为主接口/删除;面板打开时表单、三个开关与协议下拉均以服务端为准回填(integration/llm-settings.ts)。密钥提示文案改为「密钥仅保存在本机后端」。
  • OpenAPI 基线重导出(新增 3 个路径);英文词典删除假动作词条、新增对话与接口管理文案。

当日执行并通过:pytest backend/api/tests -q(126 个用例,含 llm-config 读写、五协议映射、流式/回退/门控、任务换脑与修复重试等 28 个新用例;本机需 PYTHONUTF8=1——alembic.ini 是 UTF-8 而系统 locale 为 GBK,属环境问题与本轮改动无关)、pytest agents/skills/tests(3.10 兼容 26 用例)、export_openapi.py --check、node --test(en-US 词典 5 项)、npm run check 与 npm run build --workspace @openmathmodel/web。

尚未验收(需浏览器 + 真实厂商接口):设置面板的回填/测试连接/接口列表交互、对话页流式渲染与错误提示视觉、真实 OpenAI 兼容网关与 Ollama 的实测(本轮上游全部为 MockTransport 模拟)。

2026-08-13(追加)主流厂商官方 API 预设与「模型厂商」页更新

实测反馈驱动的三轮加固与厂商预设落地:

  • 错误兜底:llm.py 把网络层异常全部归一为可执行错误信封(超时 504 LLM_TIMEOUT、连接失败 502 LLM_UNREACHABLE、重定向 LLM_REDIRECTED 提示改 Base URL、非 JSON 响应 LLM_BAD_RESPONSE 展示响应开头),不再向页面裸露 500;超时/网络失败与 429 一样触发备用接口切换;「测试连接」不再限制 max_tokens(推理模型思考预算可能远超小额上限),读超时放宽至 45 秒。
  • 路径与域名护栏:OpenAI 兼容协议在 Base URL 为裸域名时自动补 /v1/chat/completions(带路径原样拼接、显式前缀最优先);填知名厂商官网/聊天页域名(deepseek.com、chatgpt.com、claude.ai、kimi.com、x.ai、bigmodel.cn 等)时直接提示正确 API 地址(400 LLM_WEBSITE_URL)。前端把我们自己后端的 404 映射为「后端尚未加载新接口,请重启后端服务」。
  • 厂商预设(integration/llm-providers.ts):九家主流厂商官方 API 一键填入——OpenAI(gpt-5.6-sol/terra/luna)、Anthropic(claude-opus-5/sonnet-5/fable-5/haiku-4-5)、Google Gemini(gemini-3.6-flash/3.5-flash/3.5-flash-lite)、DeepSeek(deepseek-v4-pro/flash,旧 deepseek-chat 已于 2026-07-24 停用)、通义千问(qwen3.8-max,DashScope 兼容模式)、Kimi(kimi-k3)、智谱(glm-5.2/5/5-turbo)、xAI(grok-4.6/4.5)、本地 Ollama。模型名与接入地址采集自各厂商官方文档(2026-08-13)。
  • 「模型厂商」页:卡片改由预设生成(副标题=当前在售模型),「配置」跳转自定义 API 并自动填入协议/地址/模型(用户只补 Key),「已连接」状态按已保存接口的域名真实判定;智能路由下拉与输入区模型选择器同步为最新型号;无品牌资源的厂商 logo 回落首字母标。官方域名白名单扩充(api.x.ai、火山方舟、百度千帆、腾讯混元、阶跃、硅基流动等),不受「允许第三方中转站」开关影响。
  • 新增测试:网络异常信封 5 例、官网域名护栏 1 例、官方域名门控 1 例(累计 41 个 LLM 相关用例)。词典同步增删。
  • 思考过程展示:后端按协议提取推理内容(OpenAI 系 reasoning_content/reasoning、Anthropic thinking 内容块、Gemini thought: true 部件),流式经 SSE reasoning 事件转发、非流式随响应 reasoning 字段返回;前端在回答上方渲染思考块——流式期间「思考中…」shimmer 标签 + 180px 封顶自动跟随视口(超高时上下渐隐遮罩),回答开始后折叠为「已思考 N 秒」可点击展开回看;思考内容只展示、不回写对话历史;适配深色主题与「减少动态效果」偏好。无思考内容的模型在等待首个回答增量时同样显示扫光「思考中…」占位;思考头部按钮覆盖全局 button 悬停底色保持裸文本观感;回复完成后右下角提供复制按钮(复制 Markdown 原文,成功后短暂切换对勾图标)。
  • 执行步骤时间线:完成态图标由绿钩改为黑色圆形白色对钩(19px,深色主题反白),与单色设计语言一致。
  • 细粒度活动流(参考图节奏:思考/工具/叙述交替):真实 LLM 节点的每次模型调用产出过程事件进 run.log(EngineLlmPort.on_event → 同事务 append_event)——thinking(思考内容截断至 6000 字 + 耗时)与 llm_call(模型/接口/耗时/tokens 摘要);工作台回复区在摘要之后渲染 .agent-stream:run.log 过程行(深度思考=sparkle、调用模型=lightning,右侧真实耗时,点开浅灰 16px 圆角容器看思考全文或调用参数)、artifact.published(写入 X,可展开)、approval.requested/resolved(等待确认行实时走秒、落定显示等待时长)、node_changed/status_changed 作为叙述行穿插;按 sequence 去重,SSE 首连回放历史、重连不重复;step.* 事件归上方阶段时间线不重复展示。follow-tail 浮标按产品反馈移除。配套后端测试断言 thinking/llm_call 过程事件入流。
  • Agentic 执行轨迹(融合改造,参考 Codex 语义时间线/RemCodex 统一纵向流的公开设计):真实运行下(data-workspace-source="api")把运行页的六个步骤行去卡片化为时间线行——左侧动作图标(分析/数据/方案/实验/写作/交付各一,进行中呼吸脉冲,完成保持既定的黑底白钩,失败红叉)、中间阶段名(重试时加「第 n 次尝试」徽标)、右侧真实耗时(数据来自 /task-runs/{id}/steps 控制面接口,与快照并行拉取;完成/失败=服务端起止差,进行中每 500ms 走秒);行点击展开的详情区为浅灰 16px 圆角低权重容器(失败时显示失败原因,进行中显示 current_step);细左轨 + 垂直留白建立时间线关系;「回到最新」follow-tail 浮标(上翻 160px 出现,不抢滚动位置);输入框上方运行状态条(spinner + 状态 · 当前阶段 + n/6,等待确认暂停旋转,规划期与终态不显示)。演示页面完全不受影响(样式全部按 api 来源作用域隔离)。
  • 任务执行节奏(按 ADR-0007 组件语义实现,不新增容器):任务创建后、首个步骤启动前(QUEUED 或 RUNNING 且全部页面 PENDING)为规划阶段,控制器广播 omm:run-planning,页面层在同一条 Agent 消息内(注入步骤块头部之后,不拆成第二条对话)发起一次真实的开场分析(走 /api/chat:思考块流式 → 折叠「已思考 N 秒」→ Markdown 分析正文,未配置接口/未登录时整块静默消失,sessionStorage 按 run 防重、刷新不重复扣费)。严格顺序出现:开场分析落定前(data-opening-state="pending")计划部分(折叠开关/步骤列表/阶段摘要/CTA)完全不渲染也不占位——包括「任务正在排队」摘要与「等待任务开始」按钮;回复结束(成功/失败/静默移除均算)后置为 done 并广播 omm:opening-analysis-done,控制器立即重渲染,六个阶段按 140ms 级联缓速揭示(等待期间刻意不构建时间线,保留 planning 标记,放行时才播放级联;之后的 SSE 刷新与中途进页不重播)。注意 hidden 属性对带作者级 display:flex/inline-flex 的元素无效,这些元素需显式 [hidden] { display: none !important },否则会出现「已隐藏却仍显示」。
  • 对话页不暴露接口信息:回复正文不再附带实际域名、模型名与 Auto 难度判定(流式期间的「正在调用 」行一并移除);「允许使用第三方中转站」的透明化只体现为设置中心的本机用量记录,关闭时连记录也不留。
  • 对话回复 Markdown 渲染(text/markdown.ts,零依赖):转义优先的受限子集——标题/列表/引用/表格/分隔线/粗斜体/行内代码/代码块/http(s) 链接;$…$、$$…$$、\(…\)、\[…\] 公式输出为 data-tex 节点(行内加 data-tex-inline),复用方法库的 KaTeX 懒加载器在全文到齐后排版,加载前保留 LaTeX 源码回退;「$5 和 $10」这类金额不会误判为公式,javascript: 链接保持纯文本。流式期间逐增量重渲染,未闭合代码块随增量增长。配套 markdown.test.mjs 10 个用例(XSS 转义、注入路径、表格、流式半截代码块等)。

当日执行并通过:pytest backend/api/tests/test_llm_*.py test_task_runs_llm_nodes.py、node --test(词典 5 项)、npm run check 与 npm run build --workspace @openmathmodel/web。真实接口对 ainb.plus 网关探针验证连通(假密钥返回上游 401 信封);DeepSeek 官方等真实密钥实测留给浏览器验收。

2026-08-20 执行步骤揭示时机:规划真正完成后才显示

产品反馈驱动:此前「首个步骤一启动」就揭示六个执行步骤,而题意解析几乎在任务创建后立即启动,体感等于“随便发点什么都马上弹出全部步骤”。本次把揭示时机推迟到规划真正完成:

  • isPlanningPhase(modeling-workspace-controller.ts)判定窗口延长——QUEUED,或 RUNNING 且问题分析页仍为 PENDING/RUNNING 且后续页面均未启动,都属于规划阶段;问题分析页落定(成功/失败)或任一后续阶段启动才视为规划完成。运行离开 QUEUED/RUNNING(暂停、待审批、失败、终态)时需要展示状态行,同样揭示。
  • 规划阶段「收起/查看执行步骤」折叠头与步骤列表一起等待揭示(新增 .activity-summary 的规划期隐藏,两种布局通用;[hidden] 的 !important 还原规则此前已存在)。此期间用户看到的是:开场分析(若已配置接口)→「正在思考并规划执行步骤…」扫光行 + 活动流的「深度思考 · 题意解析」过程行;规划完成后六个阶段仍按 140ms 级联揭示,SSE 刷新与中途进页不重播。
  • 附带修复:此前若首个快照到达时题意解析已在运行(常态时序),omm:run-planning 不会广播、开场分析整体被跳过且步骤立即可见;判定窗口延长后该竞态消除,开场分析可靠触发(sessionStorage 按 run 防重不变)。
  • 不动契约、后端与页面结构:判定完全基于既有 ModelingWorkspaceView.pages[].status 语义(问题分析页在 PROBLEM_ANALYSIS 推进到后续节点时由投影算法翻为 SUCCEEDED)。

2026-08-20(追加)输入框附件托盘折叠样式

附件托盘(composer-attachments 模块自有槽位,覆盖首页/确认页/对话页所有 .composer)加可伸缩折叠,动效语言参考 aicss.dev To-do List、按产品黑白灰体系以原生 DOM 重写:

  • 折叠头:回形针图标 + 「附件」 + 计数徽标;悬停图标交叉淡出为箭头,收起时箭头旋转 -90°,aria-expanded 随动;点击收展,收展为 grid-template-rows 1fr ↔ 0fr + 透明度过渡。
  • 计数徽标「已解析/总数」(已解析 = phase 非 parsing/uploading):逐字符槽位滚动更新(旧字上移、新字滚入,350ms),内容为纯数字斜杠,语言中立不进词典;徽标带 aria-label。
  • 新附件按加入顺序级联入场(50ms 步进);解析进度等重渲染不重播动画(已入场 id 集合防重)。收起状态下新增附件自动展开;清空后回到展开态。
  • 单模态提醒行(ADR-0010)与错误状态行保持在折叠区外,收起时不会藏住需要知情的内容。
  • 修正 .composer-attachments[hidden]:托盘是作者级 display:flex,会压过 UA 的 [hidden],此前托盘为空时留白不可见,现在有常驻折叠头必须显式还原 display:none。
  • 全部动效尊重 prefers-reduced-motion;暗色主题按 html[data-theme="dark"] 补齐配色。

2026-08-27 厂商预设、模态识别与费用估算刷新到当代型号

按各厂商官方文档重新采集(2026-08-27),只改数据与识别规则,不动页面结构与接口契约:

  • 厂商预设(integration/llm-providers.ts):智谱换成 glm-5.3 / glm-5.3-flash / glm-5.2(GLM-5.3 于 2026-08-14 发布、08-19 开放 API,与 5.2 共用基座同价);通义千问补 qwen3.8-flash(2026-08-26 上线);Kimi 换成 kimi-k3 / kimi-k2.7-code / kimi-k2.6;Anthropic 把能力最强的 claude-fable-5 提到首位(Mythos 5 仅限 Project Glasswing,不入预设)。OpenAI、Gemini、DeepSeek、xAI 经核对仍是当前在售型号,保持不变。
  • 预设新增可选 altHosts:同一厂商的多个官方入口(智谱 api.z.ai、Kimi api.moonshot.ai)共用一张卡片的「已连接」判定与品牌标识,新增 presetMatchesHost() 供卡片状态与模型选择器共用。
  • 官方域名白名单(llm.py)补 api.z.ai、api.moonshot.ai;官网→API 提示补 z.ai / chat.z.ai / www.bigmodel.cn。
  • Auto 路由的能力推断:新一代档位记号入表——强档补 fable、-sol,轻档补 -luna(带连字符避免误伤 solar 等词根)。命中不了的型号仍按中位 5,用户填写的权重永远优先。
  • 模态识别(integration/model-modality.ts,ADR-0010):修正 Kimi K 系列误判——K2.5 起均支持图片输入,此前被当作纯文本会误报「图片不会被看到」,现改判视觉;GLM 视觉线模式放宽到 glm-Nv(覆盖 glm-5v-turbo),并单列原生多模态的 GLM-5.3-Flash;GLM-5.3 官方声明仅文本,精确入纯文本表(不做家族级推断,未知型号继续沉默)。
  • 「默认模型 ID」补全(新增 POST /api/llm/models):预设表是快照、写下来那天就开始过期,因此让输入框直接问接口本身要模型列表——OpenAI 兼容家族 GET {base}/v1/models(裸域名补 /v1,不套「路径前缀」,那是对话补全用的)、Anthropic GET {base}/v1/models(x-api-key + anthropic-version)、Gemini GET {base}/v1beta/models?pageSize=200&key=(去掉条目名的 models/ 前缀)。密钥仍只在服务端使用,不产生 token、不记用量,因而也不受预算闸门约束;上限 300 条,网关未实现(404/405)时给 LLM_MODELS_UNSUPPORTED 并指向「手填模型 ID」。前端在现有输入框上挂原生 <datalist>(UA 默认 display:none,零布局改动):先按 Base URL 域名秒填预设型号,聚焦时再拉真实清单合并(fetchEndpointModels 按「协议+地址+密钥」缓存,失败不留缓存以便重试),拉不到就只留预设,不打断填写。新增 8 个后端用例覆盖三种协议的路径与头、404 指引、中转站门控与登录校验。
  • 费用估算表(usage.py,影响预算硬闸门):按「本地零费用 → 当代型号 → 上一代型号 → 家族兜底」重新分组,补 GPT-5.6 三档、Fable/Opus 5、Gemini 3.6 Flash、DeepSeek V4 Pro/Flash(2026-08-16 起峰谷两价,按峰价保守估)、Qwen3.8-Flash、GLM-5.3/5.2、Grok 4.5/4.6 的实际单价;顺带修掉 qwen3: 本地标签被 qwen 家族兜底价抢先命中的失效条目。未收录的新型号仍落家族兜底,不会算不出钱。

2026-08-27 执行过程事件即时可见(修复「阶段静默后整批闪现」)

真实节点的一次模型调用动辄一两分钟,此前 EngineLlmPort.on_event 产出的 thinking/llm_call 过程事件只挂在推进事务里、要等节点结束的下一次 checkpoint 才随 STEP_SUCCEEDED 一起提交——SSE 是轮询已提交行的模式,结果是每个阶段执行期间工作台完全静默,结束时整批过程行同时闪现。本批修复:

  • 推进线程(checkpoint 模式)下,每条过程事件在发生的当下独立提交并立即对 SSE 可见;HTTP 动作路径保持整请求一个事务不变。
  • 新增 llm_call_started 过程事件(kind/prompt_id/repair):每次模型调用开始时发出。前端把它渲染为带实时走秒的「深度思考 · 阶段」行,thinking 到达时整体替换为含思考全文的最终行,无思考内容的模型由 llm_call 就地落定(活动流仍不展示模型与接口名)。
  • 用户刷新页面时若某次调用仍在进行,SSE 首连回放会重建这条走秒行,落点仍在首气泡活动流。

2026-08-28 接待判定纳入附件内容(修复「无关文件也会启动六阶段」)

带附件的发送此前在接待判定(POST /v1/task-intake)里被启发式直接放行(假设「附件≈题面」),内容与建模无关的文件照常创建任务,最后由问题分析节点的 viability 门以「题意解析执行失败」收场——语义上是输入问题,却被渲染成带重试按钮的工作流故障。本批修复:

  • 契约:TaskIntakeInput 新增可选 attachments[](name/excerpt/characters,单摘录上限 2000 字符、最多 20 个),OpenAPI 基线已重导出。
  • 服务端(intake.py):有正文摘录的附件必须进模型判定——提示词逐个列出文件名与摘录(每个截 500 字、最多 5 个),附件内容与输入正文同权;解析不出文字的附件(纯图片/扫描件/关闭自动解析/确认页仅元数据)维持放行,由第二层 viability 门兜底。长题面(≥200 字)仍启发式放行,不为附件多烧判定调用;判定异常一律放行的总原则不变。
  • 前端(task-start-controller.ts):发送前先等浏览器解析完成(store.settled()),把每个附件的文件名与前 1200 字摘录随判定请求上送;@ 引用赛题仍视为带着题面来的,直接放行。判定不放行时沿用既有「首页对话」路径原地回应,不建项目、不出现执行计划。
  • 测试:接待判定套件 7→10 个用例(附件摘录必进判定且意图透传、无摘录附件启发式放行不出网、长题面优先于附件判定)。

当日执行并通过:pytest backend/api/tests(255 个用例,其中 task-intake 10 个)、npm run check --workspace @openmathmodel/web(typecheck + eslint)、Web 生产构建(139 个模块)、export_openapi.py 基线重导出。

尚未验收:真实浏览器中发送「短正文 + 内容无关的附件」应停留首页并收到对话式回应(不出现执行页与计划条)的视觉走查。

2026-08-28(追加)任务创建过场、开场分析附件兜底、执行计划条目压缩

三项用户实测反馈的修复,不动页面骨架、路由与契约结构:

  • 任务创建过场遮罩(新增 integration/task-launch-overlay.ts + task-start-controller.ts 接线):此前发送后只有状态行一句「任务已创建,正在进入运行工作台…」。现在接待判定放行后全屏遮罩接管:「创建项目 → 上传附件 → 启动 Agent 工作流」随真实阶段逐步打勾(SubmitOptions.onPhase,无附件时不显示上传步骤),成功后主视觉环收拢成对勾、定格一拍再导航;判定不放行(首页对话)、未登录、出错时遮罩不出现或立即淡出,错误仍走状态行。遮罩 aria-hidden,读屏通道保持状态行(role=status);动效尊重 prefers-reduced-motion,暗色主题补齐配色;层级 230(压过设置 180 与二级弹框 220,让位 toast/菜单 260)。
  • 开场分析拿不到附件的修复(attachments/task-attachment-context.ts 重写):用户实测「上传了 PDF,首条回复却说没收到题面」。原因是任务附件上下文对服务端正文只等 12 秒、超时只留一句「仍在解析中」,且工作台清单拉取失败会被永久缓存成「无附件」。现在:① 任务创建时把浏览器解析摘录按 run 交接到 sessionStorage(persistTaskAttachmentExcerpts,单条 4000 字/合计 24000 字,与草稿摘要上限一致),服务端正文没赶上等待预算时以摘录顶上(标注「浏览器初步解析、完整正文稍后自动并入」),权威全文就绪后仍并入后续轮次(摘录已覆盖全文的短附件不重复注入);② 工作台清单拉取失败或为空不再缓存,下一轮重试,失败当轮直接用交接摘录兜底;③ 正文请求失败立即重试并计入「解析中」;④「解析中」提示列出文件名并明确指示模型「勿断言缺少题面、勿要求用户补充材料」。
  • 执行计划第 4 条压缩(workspace_view.py _plan_texts):实验与验证条目此前两处发胖——初稿把 EXPERIMENTING 与 VALIDATING 两句拼接,方案确认后又细化为「按方案「名称」实施:步骤1;步骤2;步骤3」(上限 240 字),远长于其他条目(12~24 字)。现在多阶段页面只取首个阶段的短句;细化只保留「按方案「名称」实施」(方案名截 18 字,整条截 40 字兜底),步骤全文仍在建模方案页与「实现计划」分页。对应测试断言同步更新(所有 plan_text ≤ 40 字)。

当日执行并通过:pytest backend/api/tests(完整套件)、npm run check --workspace @openmathmodel/web(typecheck + eslint)、Web 生产构建(140 个模块)。

尚未验收:真实浏览器中的三项视觉走查——发送后过场遮罩的完整节奏(含无附件/出错/未登录分支)、带附件任务的开场分析首条回复内容、执行计划面板各条目长度。

2026-09-05 智能路由候选改为已保存接口;GPT-6 Astra 适配

产品反馈:设置中心 → 模型厂商 → 智能路由的四个下拉「问题不是写不写死,而是要看用户自己配了什么」,并且要能即时更新;OpenAI 新旗舰 GPT-6 Astra 需要适配。不动页面结构、路由与接口契约(ADR-0014 决策 11):

  • 智能路由下拉(openmathmodel-ui.ts):删掉四组写死的厂商型号,改为 routingSelectOptions(config) 生成——自动选择 + 每条已保存接口(取值 endpoint-<id>,与输入框模型选择器同一套标识;显示「模型 · 接口名」,主接口再标一下)。新增 renderLlmConfigViews 把服务端接口配置的三处投影(已保存接口列表、厂商卡片状态、智能路由候选)一起刷,hydrateLlmPanel 打开面板、「保存为新接口」「保存修改」、菜单里的设为主接口/删除/调整权重之后都走它,用户不必关掉设置再打开。刷新保留当前选择,接口被删回落 自动选择;首次打开时选择从本机设置表读取(那时原生 select 只有 Auto 一项,restoreSettings 写不进去)。池子为空/未登录时四个下拉只剩 自动选择,下方 [data-routing-note] 一行说明指向「自定义 API」或登录(新增 .settings-field-note,revision 37)。
  • 自定义下拉增强(enhanceSettingsSelect)从 openSettingsCenter 闭包提为可重复调用的模块级函数:重建前先拆掉旧的 .settings-custom-select,选项集合动态变化时原生 select 与自定义下拉不再分叉。
  • 边界如实标注:这四项在本轮只随整张设置表落本机 localStorage,服务端不读取它们;服务端链路由同日的下一条记录(ADR-0015)补上。
  • 明确保留:设置中心「自定义 API」表单的预填示例(api.example.com / 示例密钥 / gpt-5.6-sol),endpointFromForm 对 example.com 直接判为未配置。
  • GPT-6 Astra(2026-09-03 分批 GA,API id gpt-6-astra,1.05M 上下文、文本+图像输入,标准价 $10/$50,>272K 输入整笔 2×/1.5×):厂商预设(llm-providers.ts)OpenAI 首位改为 gpt-6-astra,「配置」一键填入即为它;模态表(model-modality.ts)/^gpt-5/ 放宽为 /^gpt-[5-9]/,Astra 判为视觉;Auto 路由能力推断(llm.py)强档记号新增 -astra(否则旗舰按中位 5 使用);费用估算表(usage.py)新增 gpt-6-astra / gpt-6 70/350 元(此前会落到 4/12 元的兜底价,预算闸门对旗舰放行十几倍)。
  • 词典:删「根据任务类型、速度与费用自动选择模型。」,增智能路由说明与两条空态提示。

当日执行并通过:npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web、node --test src/i18n/en-US.test.mjs(5 项)、pytest backend/api/tests/test_llm_chat.py test_usage.py test_budget_guard.py(79 个用例,含新增 test_endpoint_strength_recognizes_current_flagships、test_model_pricing_covers_gpt6_astra)。

尚未验收(需浏览器):登录后在「自定义 API」保存/删除接口时切到「模型厂商」看四个下拉是否即时跟随;未登录与零接口两种空态提示;OpenAI 卡片副标题与「配置」默认模型为 gpt-6-astra。

2026-09-05 智能路由做实:按任务类型把模型调用定向到指定接口(ADR-0015)

业主确认要让四个下拉真正生效。契约与语义见 ADR-0015,落点:

  • 契约(schemas.py / routers/account.py):LlmConfigUpdateRequest 增 smart_routing: bool = True 与 task_routes: TaskRoutesModel(coding / research / writing / vision 四个可空 id);PUT 用 normalize_task_routes 只存指向本次保存里现存接口的项,GET(llm_config_payload)永远回四键齐全、指向已删除接口的项读出为 null。OpenAPI 基线已重导出(export_openapi.py --check 通过)。
  • 解析与取链(llm.py):LlmConfig 增 smart_routing / task_routes,route_for(kind)(开关关闭、未定向、接口不存在 → None)与 chain_for(kind)(有定向 → chain_from(定向接口),否则 chain());预算硬限制把付费接口过滤掉后指向它们的定向自然失效(find 找不到)。
  • 任务执行(engine_glue.py / EngineLlmPort):新增 _PROMPT_TASK_KINDS(与 _PROMPT_NODE_IDS 同键:题意解析 / 数据准备方案 / 方案设计四条 → research;清洗沙盒 / 实验代码 / 检验 → coding;论文四条 → writing),端口按 prompt_id 取 chain_for(kind) 传给 stream_complete_with_fallback / complete_with_fallback;llm_call 过程事件附 task_kind 与 pinned,工作台执行轨迹与 evals 可核对「这一步打给了谁」。接待判定与 Auto 难度判定不受影响。
  • 对话(routers/chat.py):携图且未显式 endpoint_id 时优先走 route_for("vision"),meta route = {mode: "task", kind: "vision", endpoint_id};用户显式钉接口不改。顺手修掉 route_meta["difficulty"] 的硬取键(非 Auto 路由的 meta 没有该键会 KeyError)。
  • 前端:新增 integration/task-routes.ts(下拉值 ↔ 接口 id、taskRoutesFromForm、normalizeTaskRoutes、resolveTaskRoute,纯函数,task-routes.test.mjs 5 项);auth/api.ts LlmConfig 增可选 smart_routing / task_routes;llm-settings.ts 新增 routingFromForm,「保存更改」「保存为新接口」与三个行为开关同一纪律一并携带(否则整体替换式 PUT 会把服务端已存的定向抹成缺省);model-modality.ts 的 resolveSelectedModality("auto") 先看 vision 定向再看主接口,并暴露 invalidateModalityConfig(),hydrateModelPickers 每次接口/路由变更都作废缓存;设置面板 renderRoutingSelects(backdrop, config, { fromServer })——打开面板以服务端 task_routes 回填、面板内刷新保留未保存选择,hydrateLlmPanel 同时回填 smartRouting 开关;开关文案改为如实描述(「开启后按下方任务类型把调用定向到指定接口;关闭则全部走主接口链」),区块说明补一句「保存后任务的对应阶段与携图对话按此定向」。
  • 新增 backend/api/tests/test_task_routes.py(10 个用例:规范化与解析、chain_for、提示词全覆盖归类、llm-config 往返与删接口清理、端口按任务类型选链与开关关闭、携图对话的 vision 定向 / 显式接口优先 / 流式 meta)。

当日执行并通过:pytest backend/api/tests(320 通过、2 跳过;4 个失败全部位于 test_stage_outputs / test_task_runs_llm_nodes 的稳健性复跑与 G3 结果闸门,属于工作树里另一路在进行中的 H3 验证节点改动——test_task_runs_llm_nodes.py、stage_outputs.py、validating.*.prompt.md 均处于修改态——与路由无关)、export_openapi.py --check、npm run check、npm run build、node --test src/i18n/en-US.test.mjs src/integration/task-routes.test.mjs(10 项)。

尚未验收(需浏览器):ADR-0015 验收 4(打开面板以服务端为准回填、保存后重开仍是改后值、删除被定向接口后回落自动);验收 2/3 需要两条真实接口(可用两个本地 Ollama 模型)跑一次任务与一次携图对话,看 run.log 的 llm_call.pinned 与对话 meta 的 route.mode。

2026-09-05 报障修复:「重试当前阶段」后页面冻结;中转站自身故障一次判死

用户报障:题意解析失败后点「重试当前阶段」,页面停在「进行中 + 正在思考并规划…」再无变化。直接查本地库对账(run_e761cc88):22:03:05 FAILED → 22:03:23 RUN_RETRIED → 22:03:25 attempt 2 → 22:03:53 再次 FAILED,服务端整个过程正常,页面一条都没收到。

  • 前端(modeling-workspace-controller.ts):运行到终态时服务端发 stream.end,控制器置 streamEnded 并关闭 EventSource、不再重连;retry 成功后 refresh() 拿到 RUNNING 却没有重接事件流,attempt 2 的 step.started / llm_call_* / 再次 FAILED 全部丢失。ADR-0013 修订重开已为同一问题修过一次(omm:run-reopened),retry 这条路漏了。修法:抽出 reopenStream(),refresh() 在快照为非终态且流已收尾时重连(after=lastSequence 只接增量,replayThrough 不变所以新事件按实时落位),onRunReopened 复用同一函数。§6 SSE 规则补了这一条。顺带修 .todo-count[hidden](display:inline-flex 压过 UA 的 [hidden],思考态下旧的「0/6」一直挂着)。
  • 后端(llm.py / engine_glue.py):这次两轮失败的正文分别是 HTTP 401「鉴权服务请求失败: … context deadline exceeded (Client.Timeout …)」与 HTTP 500「failed to connect to user=postgres …: FATAL: sorry, too many clients already」——中转站自家鉴权微服务超时、数据库连接池打满,与密钥、模型、请求内容无关。旧逻辑把非 402/429 的上游 4xx/5xx 一律归 LLM_UPSTREAM_ERROR(确定性):不切备用接口、引擎整次调用不退避重试,题意解析一次就把任务判死,指引还让用户「检查接口配置」。现在 _upstream_error 按状态码 502/503/504 或正文命中网关故障特征词(_UPSTREAM_TRANSIENT_HINTS:deadline exceeded / client.timeout / timed out / timeout / too many clients|connections / connection refused|reset / temporarily unavailable / service unavailable / overloaded / server is busy / 鉴权服务请求失败 / 请求超时 / 服务繁忙 / 系统繁忙)归为新码 LLM_UPSTREAM_UNAVAILABLE(HTTP 502,文案「接口 X 暂时不可用(HTTP 原状态码):正文」),同时进 _FALLBACK_CODES(对话与任务都会切备用接口)与 _TRANSIENT_CODES(引擎整次调用 2s/5s 退避重试);_API_ERROR_GUIDANCE 新增对症指引(与余额、密钥无关,已自动重试仍失败→稍后再试或加备用接口),_FAILURE_CLASS_RULES 补「暂时不可用」。「invalid key」式 401、「boom」式 500、404/400 仍是确定性失败,既有测试语义不变;普通 500 只有正文命中特征词才按瞬态处理。
  • 新增 backend/api/tests/test_llm_upstream_transient.py(19 项:两组参数化分类、402 独立码、失败归类与指引、非流式/流式链内回退、密钥错不烧备用接口、单接口配置下引擎退避重试恢复)。

当日执行并通过:pytest 新文件 19/19;test_llm_chat.py、test_llm_engine_retry.py、test_llm_config.py、test_task_routes.py、test_budget_guard.py、test_task_intake.py 共 104 通过;test_task_runs_llm_nodes.py -k "failure or failed or retry" 5 通过;npm run check、npm run build(index 723.91 kB)。浏览器复现验收(失败 → 点重试 → 页面出现第二次尝试行与最终状态)待用户;后端进程需重启才会加载新的瞬态判定(npm run dev 启动的 uvicorn 不带 --reload)。

2026-09-05 报障修复:生成中切页,提问与半截回复整轮消失

用户报障:在任务页追问后没点暂停就切到别的页面(或从有历史的任务切到另一条记录再回来),那一轮的提问、已生成/已思考的部分全部不见;没完成过一次对话的任务页上只剩阶段失败摘要,看起来像"凭空冒出一句话"。

  • 根因:站内除工作台五个阶段面板之间是软切换外,所有跳转(go() → window.location.href、侧栏「最近任务」的裸 <a href>)都是整页导航,浏览器直接掐断在途的 POST /api/chat 流并销毁内存;而本机对话记录(tasks/conversation-log.ts)只在一轮回复完整收到后才成对写入,agent-chat 的在途登记(pendingTurns + omm:chat-turn-committed/failed)又是纯内存的,只救得了软切换。失败的一轮(接口报错 / 后台失败)同样不落盘。
  • 修法(不改导航方式):在途轮做成可恢复的持久状态。
    • conversation-log.ts:新增按归属隔离的在途轮记录 openmathmodel.chatPending.v1.<scope>(PendingTurnRecord:owner 标签页 id、用户消息、附件名、半截 reply/reasoning、时间戳),save/load/clearPendingTurn;settleInterruptedTurn() 把它落定为正式条目:用户消息 + 一条 interrupted: true 的回复(保留半截正文与思考,note 记原因,≤300 字)。parseConversationLog 只对 interrupted 回复放行空正文。clearConversationLog / clearAllConversationLogs 一并清在途记录。
    • agent-chat.ts:sendConversationTurn 发起即写在途记录(「保存任务历史」开启时),delta/reasoning 节流 500ms 回写;pagehide 那一刻把所有在途轮同步落定为「回复在离开页面时中断,以上为离开前已生成的部分…」/「…尚未收到内容;请重新发送。」;成功清除;失败落定(note=「回复生成中断:<错误>」,用户暂停且一字未收 =「已暂停生成。」)。configureConversation 绑定归属时 settleStalePendingTurn():同标签页留下的或超过 15 分钟无心跳的在途记录落定(软切换回来仍活着的、别的标签页正在生成的不碰)。history 重建:被打断且无正文的一对不进模型上下文(保住 user/assistant 严格交替),有半截正文的照常进入。开场分析被打断只清记录不留条目(规划阶段重进会按既有防重逻辑重发)。
    • 页面层:任务页 appendRestoredReply 与首页 restoreHomeChat 识别 interrupted 条目——半截正文照渲染、思考照回看盒、下方一行 .muted.reply-interrupted 灰字说明(workflow-refresh.css:有正文时虚线隔开),无正文不挂复制按钮;restoreHomeChat 读记录前先 settleStalePendingTurn。en-US 词典补两句。
  • 契约与后端不动:/api/chat 依旧无状态,服务端不知道也不需要知道这轮被掐断。

当日执行并通过:node --test src/tasks/conversation-log.test.mjs(15 项,新增 interrupted 解析、在途记录往返、落定追问/开场、清理联动)与 src/i18n/en-US.test.mjs;npm run check;npm run build(index 728.06 kB)。浏览器复现验收待用户:任务页追问 → 思考/生成中点侧栏另一个任务 → 回来应看到自己的提问 + 半截回复(或空回复)+ 灰字说明;首页对话同理。

用户驳回(同日):「不能光靠一句子来解决。你数据没有存好也没有做到持续性。」——上面这套「留痕」方案已被下一条整体替换;chatPending 在途记录与 pagehide 落定机制已移除,本机记录降级为只读兜底。

2026-09-05 用户驳回后重做:对话生成改为服务端托管的后台作业,记录落库(ADR-0016)

用户要求的是持续性(切走再回来,回复应该完整或仍在生成)与数据存好(不是浏览器内存/localStorage),不是一句中断说明。根因是「浏览器是生成的宿主」:/api/chat 无状态转发,页面一卸载生成就死。重做为:

  • 后端
    • orm.ChatTurnRow / 迁移 0019_chat_turns:chat_turns(id, user_id, scope_id, status, opening, text, attachments, reply, reasoning, meta, error_code, error_message, trace, created_at, updated_at, ended_at),索引 (scope_id, created_at);test_migrations 的 ORM↔迁移一致性照常约束。
    • omm_api/chat_turns.py ChatTurnHub(app.state.chat_turns,进程内单例):start 建行后在守护线程里跑 llm.stream_events,事件按 seq 缓冲、正文/思考每秒批量回写库、终态后缓冲保留 10 分钟;stream_live(after) 用条件变量推事件 + : ping 心跳;stop 立即定格为 stopped(工作线程丢弃上游迟到事件并关连接);recover_interrupted() 在 lifespan 启动时把遗留 running 标 interrupted(文案「服务重启,本轮生成中断;需要完整回答请重新发送」);persist=False 时只在内存托管(「保存任务历史」关闭);delete_scope(s) / delete_user_turns 供删除与隐私联动;用量只在正常完成时记一次(partial/stopped 不记)。
    • routers/chat.py:把配置/预算/视觉/端点校验抽成 _prepare_call,Auto 路由判定延后到后台线程里(_resolve_chain),用量记账钩子 _usage_hooks 用独立会话——旧 POST /api/chat 复用同一套、行为不变(67 项既有测试通过)。新增 POST /turns(202)、GET /scopes/{scope}/turns、DELETE /scopes/{scope}、GET /turns/{id}、GET /turns/{id}/events?after=、POST /turns/{id}/stop、PATCH /turns/{id}(trace)。schemas.ChatTurnStartRequest(scope_id 形如 ^(run|chat)_[A-Za-z0-9]{6,40}$)与 ChatTurnTraceRequest。SSE 生成器不再触碰请求作用域的 DB 会话(FastAPI 在流式开始前就会关闭依赖会话)。
    • 联动:privacy._delete_runs 先删 chat_turns(项目删除级联);PUT /api/account/privacy-settings 由开到关时 delete_user_turns。
    • 新增 backend/api/tests/test_chat_turns.py(17 项:后台完成落库 + 单条用量、无观众照常完成、after= 续播、停止冻结半截并关上游、停止且一字未收 → GENERATION_STOPPED、上游 500 → failed、409 并发、开场轮空 text、trace 往返、非本人运行 404、chat_ 归属按用户隔离、非法归属 422、历史关闭 → 只在内存、关闭历史删库、删归属停生成、删项目级联、重启标 interrupted)。
  • 前端
    • 新增 integration/chat-turns-api.ts(类型化客户端 + SSE 解析 readChatTurnEvents);agent-chat.ts 重写为托管轮路径:sendConversationTurn 有归属 → runHostedTurn(建轮 → onTurnStarted → followTurn:附着、AbortSignal → 服务端 stop、断线按 after=seq 重连 ≤5 次、失败拉视图补正文、settleTurn 统一收口),无归属 → 旧无状态路径;新增 hydrateConversation(scope)(拉全部轮,已定格的进 history)、attachConversationTurn(turn, handlers, signal)(续接 running 轮)、entryFromTurn(轮 → 页面条目:stopped/failed/interrupted/中途出错 → interrupted + note)。移除 pendingTurns / pagehide / localStorage 写入 / omm:chat-turn-committed|failed。
    • modeling-workspace-controller.ts:首个快照到手 → configureConversation → await hydrateConversation(run) → 广播 omm:conversation-restore{runId, goal, turns} → 才 renderWorkspace;并发刷新同样等这一步(conversationReady Promise),规划阶段不会重复发起开场分析。
    • legacy/openmathmodel-ui.ts:omm:conversation-restore 先渲染本机旧记录,再按时间重建服务端的轮(开场轮 → restoreOpeningReply;running 开场轮 → resumeOpeningTurn 原位续播并落防重标记;追问轮 → 用户气泡 + appendRestoredReply(entryFromTurn);running 追问轮 → resumeFollowUpTurn);抽出 createReplyPresenter(思考块 + 流式 Markdown + 域名透明行 + Auto 难度行)、settleGeneratingRow、presentReplyFailure 供首发 streamAssistantReply 与续接 attachAssistantReply 共用;轨迹行改为回复完成后 PATCH /turns/{id}(不再写 localStorage);appendReplyTraceRow 支持 startedAt(续接的生成计时从服务端建轮时刻起算);omm:run-planning 不再读在途记录,只看防重标记与页面上是否已有开场块。
    • home-chat.ts:restoreHomeChat 改为 async(本机旧记录 + hydrateConversation(chat),running 轮续接直播),首发与续接共用 presentReply;task-start-controller.ts 相应改为 .then(ok => !ok && resetHomeChat())。recent-tasks.ts 删除首页对话时先 DELETE /api/chat/scopes/{chat}(文案改为「全部消息将被清除」)。
    • tasks/conversation-log.ts 精简为只读兜底(parse/load/clear*,导出 sanitizeTrace),删除 PendingTurnRecord 全家;测试 8 项。en-US 词典:删两句已废文案、补「已暂停生成,以上为暂停前已生成的部分。」「回复在生成中出错,以上为出错前已生成的部分。」「服务重启,本轮生成中断;需要完整回答请重新发送」「回复仍在服务端生成中,正在续接…」。

当日执行并通过:pytest backend/api/tests 全量 360 通过 / 2 跳过(含新文件 17/17、test_migrations、test_llm_chat);npm run check;npm run build;node --test "src/**/*.test.mjs" 107/107。受保护的 React 入口未改。后端进程需重启(npm run dev 的 uvicorn 不带 --reload;开发库由 create_all 自动建 chat_turns,部署环境跑 alembic upgrade head)。浏览器验收待用户:任务页追问 → 思考中点侧栏另一个任务 → 点回来应看到完整回复或仍在直播的半截 + 后续增量;刷新同理;暂停键应真正停止服务端生成(GET /turns/{id} 状态 stopped)。

同日实机联调(对本机 uvicorn --reload 的开发后端 + PostgreSQL,用本机模拟 OpenAI 协议 SSE 服务和一次性用户,不入库的临时脚本)23/23 通过:观众 1.6s 后断开 → 无观众继续生成 → after=seq 续接只补尾段、拼起来等于完整回复 → 落库行一致;stop 保留半截且 ~1s 内关掉上游连接;运行中再建轮 409;touch main.py 触发 uvicorn 重载后遗留轮变 interrupted(半截保留、事件流只回 GENERATION_INTERRUPTED);用量只记完成轮;DELETE /scopes/{id} 级联清空。开发库 alembic_version 由 0018 手动 stamp 到 0019(表已由 create_all 建出)。踩坑:Windows 系统代理会让 httpx 默认把 127.0.0.1 也走代理(502 空响应),测试客户端需 trust_env=False;后端 llm._direct_mounts 早已对本机目标绕过代理。

2026-09-06 模型厂商型号与单价改为服务端同步的模型目录(ADR-0017)

用户看到「模型厂商」卡片仍是 claude-fable-5 / gemini-3.6-flash,而 Claude、Gemini 都已上新:「难道每次都得重新写前端吗?不能实时同步上吗」。此前型号(llm-providers.ts PROVIDER_PRESETS[].models)与单价(usage.py PRICING)都是代码里的快照,上一次手工更新是 09-03。改为服务端从公共目录同步(数据源、裁剪规则与取舍见 ADR-0017,接口与字段见 §5.7):

  • 后端
    • 新增 omm_api/model_catalog.py:厂商预设 PROVIDERS(唯一的型号事实来源,含内置兜底快照)、reduce_catalog(models.dev api.json → 8 家厂商的可对话型号 + 全目录单价索引)、highlights_of(最新三款正式版,读取时现算)、ModelCatalog(文件缓存 data/model-catalog.json + 后台线程按 TTL 刷新 + view() / pricing_cny() / vision();失败保留旧数据并记 error,强制刷新最小间隔 60 s,CACHE_VERSION 变化即作废旧缓存)、进程级访问点 set_current/current。
    • config.py 新增 model_catalog_enabled / model_catalog_url / model_catalog_ttl_seconds / model_catalog_timeout_seconds / model_catalog_cache_path / usd_cny_rate;main.py 在 create_app 建目录并 set_current,lifespan 启停后台线程。
    • routers/chat.py llm_router:GET /catalog、POST /catalog/refresh(要求登录;关闭同步 409 MODEL_CATALOG_DISABLED;拉不到 502 MODEL_CATALOG_UNREACHABLE)。
    • usage.model_pricing:先按模型 ID 精确命中目录单价 × 汇率,再走手写前缀表,再兜底价;手写表降级为「目录没有时的兜底」。
    • 新增 tests/test_model_catalog.py(16 项:非对话 / 已下线过滤与新在前排序、Gemini 卡片只收 gemini 家族、亮点正式版优先 + 测试版垫后 + 别名不进亮点、DashScope 只收 qwen 家族、智谱备用键、单价索引官方优先 + 别的平台补缺 + 不过滤已下线、空 / 无关目录拒收、刷新写缓存 + 重启读缓存不出网、旧版本 / 损坏缓存忽略、失败保留旧快照并报错、TTL 与强制刷新间隔、单价换汇与大小写、usage.model_pricing 目录优先、接口未登录 401、关闭同步时内置快照 + 409、refresh 返回新视图)。conftest 夹具 model_catalog_enabled=False 且缓存路径指向 tmp_path。
  • 前端
    • auth/api.ts 新增 CatalogModel / CatalogProvider / ModelCatalogView 与 getModelCatalog / refreshModelCatalog;新增 integration/model-catalog.ts(会话内缓存、refresh 替换缓存、currentModelCatalog 同步读)与 integration/model-catalog-view.ts(纯函数:providerHighlights / providerModels / catalogVision / catalogFreshnessText)+ model-catalog-view.test.mjs(4 项)。
    • integration/llm-providers.ts 去掉全部模型名,只留品牌骨架;legacy/openmathmodel-ui.ts:卡片副标题占位「正在同步型号…」→ 打开设置时 loadModelCatalog 填亮点;区块标题下 [data-catalog-status] 新鲜度行;标题右侧「同步型号」按钮(refresh-catalog,withBusyButton,失败 toast 并保留上一份);「配置」默认模型 = 亮点首位;catalogModelsForHost 取代 seedModelOptions,refreshModelOptions 合并接口自报清单;hydrateLlmPanel 回填时目录晚到会补一次补全。styles.css 新增 .settings-heading-actions 与状态行换行样式。
    • integration/model-modality.ts:resolveSelectedModality 两条分支先查目录 vision(classifyModel),未收录再回落命名规则。en-US 词典补 13 条。
  • 不动:App.tsx / screens.tsx / OpenMathModelScreen.tsx;页面结构、路由、DOM 槽位;Auto 路由能力推断 endpoint_strength。

当日执行并通过:pytest backend/api/tests 全量 377 通过 / 2 跳过;npm run check;npm run build(index 735.61 kB);node --test "src/**/*.test.mjs" 117/117。实机(本机 uvicorn --reload 开发后端自动重载):启动后约 1 s 完成首次同步并落 data/model-catalog.json(≈156 KB,8 家厂商、3170 条单价);一次性用户 GET /api/llm/catalog 显示 Anthropic claude-fable-5-1 / claude-opus-5 / claude-sonnet-5、Google gemini-3.8-flash / gemini-3.7-flash / gemini-3.5-flash-lite、通义 qwen3.8-flash / qwen3.8-max / qwen3.7-plus、智谱 glm-5.3-flash / glm-5.3 / glm-5.2、xAI grok-4.6 / grok-4.5 / grok-4.3;POST /catalog/refresh 200 并更新 synced_at;未登录两条都 401。踩坑:第一版按 family 取「每家族最新」在真实数据上把 gemma-4-26b / qvq-max / glm-4.7-flash 顶进卡片,改为按时间取最新 + Google 只收 gemini 前缀;亮点改为读取时现算,缓存文件版本升到 2 强制重拉。浏览器验收待用户(本环境无浏览器):卡片副标题与状态行、「同步型号」按钮、「配置」默认模型、模型 ID 补全。

2026-09-06 报障修复:任务出问题后对话里说「继续 / 重做」只是普通聊天(ADR-0018)

截图现场:「机器人竞技攻击策略优化」实验运行阶段 FAILED,用户在聊天框说「怎么又失败了,快继续想办法」「继续啊」——前端只把消息 POST /notes(终态 409)、标「任务已结束,本条按问答处理」并挂一个对失败运行必死(RUN_NOT_COMPLETED)的「按这条要求继续修改」按钮,模型也不知道运行状态。根因:对话通道只有被动的备注注入、没有执行权,且 §11.3 旧决策明文「重做由人显式操作,绝不由备注文本触发」。用户拍板:分级执行(retry / resume / pause / 审批选项直接做;取消先确认)、已完成运行仍开修订门但点名阶段预选、失败运行的「选起点重做」留下一刀。改动(接口与页面行为见 §5.8,决策见 ADR-0018,设计文档 §11.3 已回写 v3.34):

  • 后端
    • 新增 omm_api/run_control.py:legal_actions(与状态机 / ADR-0013 同判据)、load_context(待审批门选项、修订轮数、上一轮待确认提案)、decide_locally(词表规则:重试 / 恢复 / 暂停 / 取消 / 退回 / 点名选项 match_option / 点名阶段 explicit_stage;否定词、「继续说」类对话延续、问句标记)、judge_intent(池里最弱接口 complete_once,10 s / 200 token,只认合法集内结果)、decide(本地 → 出网门槛:≤200 字、带合法动作的气味词、非问句)、execute(走 execute_action / accept_revision / record_run_note;ApiError → rejected 回执)、prompt_block(状态块 + 「不得声称执行了未列出的操作」)、run_control_step(独立 session、一个事务;任何异常回落为无动作)。
    • engine_glue.py 抽出 accept_revision(session, run, text, stage=None)(request_revision 可点名推荐起点),routers/task_runs.py 的 /revisions 与 /notes 改为委托(record_run_note 落到 run_control)。
    • routers/chat.py start_chat_turn:run_… 归属且非开场的轮,producer 先跑控制步骤、yield action 事件、把状态块追到 system 提示词后再 _resolve_chain + stream_events;上一轮 meta.actions 作为 previous_actions 传入。chat_turns.py _run 把 action 事件累进 live.meta["actions"] 随轮落库。
    • 新增 tests/test_run_control.py 23 项(合法动作集;失败 / 暂停 / 执行中 / 审批门 / 修订门 / 已完成的本地规则与反例;字母 / 序数 / 别名选项点名;提案确认 / 放弃 / 作废;判定回复解析只认合法结果;出网门槛;提示词块;e2e:「怎么又失败了,快继续想办法」→ 运行 RUNNING + 备注落库 + 首事件 action + meta.actions 持久化 + 系统提示词带执行后状态且未出网判定;问句只聊不动;取消两轮确认 / 放弃 / 提案过期;「同意,按这个方案做」选定 G1;已完成运行「从数据准备重做,…」开门并预选 → 「确认」重跑;弱模型判定生效;判定 500 回落对话;chat_ 归属不经控制)。e2e 钉 OMM_AGENT_NODES=sim 让引擎仍走模拟链、mock 只服务对话与判定。
  • 前端
    • chat-turns-api.ts RunControlAction / ChatMeta.actions;agent-chat.ts ChatHandlers.onAction、applyEvent 处理 action、续接重放 meta.actions、entryFromTurn 回执兜底重建轨迹;新增 integration/run-control-view.ts(纯函数)+ run-control-view.test.mjs(4 项,含「所有标题静态且有英文词条」)。
    • legacy/openmathmodel-ui.ts:presentRunControlAction(回执 → 轨迹行,插在生成行之前;proposed 挂「确认执行」= 发一句「确认」;executed 广播 omm:run-reopened);恢复态最后一轮的待确认提案照挂按钮(按落盘轨迹下标定位,不比文本——语言切换会翻标题);删除发送前 postRunNote 与 offerRevisionCta 死路行;en-US 补 13 条。
  • 不动:App.tsx / screens.tsx / OpenMathModelScreen.tsx;页面结构、路由、DOM 槽位;引擎状态机与 /actions 端点形状(OpenAPI 未变)。

当日执行并通过:pytest backend/api/tests(见下方全量结果);npm run check;npm run build;node --test "src/**/*.test.mjs" 125/125。浏览器验收待用户(本环境无浏览器):失败态说「继续」后轨迹行「已重试阶段 · 实验运行」+ 工作台状态即时变为执行中;「取消任务」→「等待你确认操作」行带按钮 → 点按钮或回「确认」→ 已取消;已完成态「从数据准备重做…」→ 待确认事项里数据准备预选。

2026-09-06(追加)用户追问:不能只靠关键词;任何要执行的修改都要真正回退重做(ADR-0019)

用户原话:「你现在只是通过固定的关键词来识别判断吗?agent 自己没有相关的意图识别吗?我希望不管说什么,只要是需要执行的,都会出现真正的修改,而且这个修改显示的进度,也不一定要接着上一次的显示啊」。如实答复的现状:规则先判、弱模型判定被 ≤200 字 / 气味词 / 非问句三道门槛拦住、判定看不到上文、动作被状态机锁死(FAILED 只能 retry,RUNNING 下的修改要求只静默落备注)。用户拍板:规则只做第一层门槛、模型识别必须有;同一运行从选定阶段重做(不另起新运行);重做先提案再确认。改动(接口与行为见 §5.9,决策见 ADR-0019,ADR-0018 §1 / §2 / §5 标注已修订):

  • 引擎(agents/core):新增 EventType.RUN_REDO;reducer _on_run_redo(合法源 = 工作态 / FAILED / NEEDS_REVIEW;在途 RUNNING 步骤 → CANCELLED superseded: redo from <target>;清 review / failure / paused / cancel_requested;state = target + force_rerun + _discard_from(target));TaskRunEngine.redo(snapshot, target_state, reason, note_id)(COMPLETED / CREATED 拒绝);can_transition 前向矩阵不放宽(states.py 注明回退边只在 RUN_REDO reducer 里)。agents/evals CONTROL_FLOW_FIELDS 登记 RUN_REDO(target_state, from_state)。测试:test_engine.py +3(FAILED 回退更早阶段、在途步骤被取代且丢弃评审、拒绝边界),test_replay.py +1(两次 redo 后重放 == 实时快照);core 130 / evals 48 通过。
  • 后端:engine_glue.py RUN_REDO 投影 + REDO_STATUSES + redo_run(session, run, stage, text)(409 RUN_NOT_REDOABLE / 422 INVALID_STAGE / 422 EMPTY_TEXT;备注先 flush 再发事件;run.log redo_requested);runner.py _superseded_in_flight + WorkflowAdvancer.advance 只吞 run_domain_events seq 唯一约束冲突并 warning,其它 IntegrityError 照抛;run_control.py:ACTION_KINDS + redo、CONFIRM_REQUIRED = {cancel, redo}、legal_actions(..., revision_gate=)(FAILED→{retry, redo};RUNNING / PAUSED / 节点闸门 + redo;修订门不开)、本地规则「点名阶段 + 修改 / 重试词」→ redo(点名失败阶段折 retry)、decide 改为「提案回应 → ≤12 字规则直判 → 模型(≤800 字,带最近三轮对话)→ none 回落规则」、_judge_prompt 带阶段进度 / 最近对话、_parse_judge_reply(user_text=) 阶段缺失按正文推断 / 未开始阶段 → none、redo 提案 / 执行 / 放弃回执;routers/chat.py 从 hub.list_scope 取最近三轮 {text, reply} 传 history。
  • 后端测试:test_run_control.py 28 项(重写出网门槛用例为「快路径 + 全覆盖 + none 回落」;新增本地 redo 规则、判定 redo 解析边界、e2e:FAILED 上一句无词表词的「换成随机森林…」经模型判 redo → 提案(状态不变、不落备注)→ 确认 → RUNNING@建模方案、备注正文 = 原话、tick 后 attempt 2 到 G1;G1 门上「从数据准备重做」→ 确认 → 门 CANCELLED、数据准备 attempt 2;未开始阶段的要求静默落备注;「算了」放弃;判定提示词带「最近对话」);新增 test_run_redo.py 5 项(redo_run 回退 + 备注 + run.log;状态 / 输入边界;_superseded_in_flight 两种方言;真在途让位:模拟节点执行中途从另一会话 redo_run → 本 tick 返回 None + warning、步骤行 CANCELLED、下一 tick 从目标阶段 attempt 2 起跑、不触发 executor lost;无关 IntegrityError 照抛);test_chat_turns.py 的上游 mock 统一把判定请求答 none(判定不再被门槛拦后,阻塞流会被判定调用吃掉前几段)。全量 pytest backend/api/tests 419 通过 / 2 跳过(PG 专属)。
  • 前端:run-control-view.ts redo 标题「已从阶段重做」/ 类别「从阶段重做」,提案与放弃行后缀带阶段名;chat-turns-api.ts RunControlAction.text;run-control-view.test.mjs +1(5 项);en-US +1。npm run check / npm run build / node --test 129/129 通过。
  • 不动:受保护入口、页面结构与 DOM 槽位、/actions 端点形状(无新 HTTP 端点,重做只经对话确认触发)、契约 Resolution 形状。

浏览器验收待用户:失败态说一句不带任何固定词的修改要求(如「换成随机森林比较靠谱」)→ 轨迹行「等待你确认操作 · 从阶段重做 · 建模方案」带按钮 → 确认 → 「已从阶段重做 · 建模方案」+ 工作台进度回到建模方案继续、上游两段保留;执行中说「从题意解析重做」→ 确认 → 当前步骤标已取代、下一步从题意解析起。

2026-09-07 报障修复:对话触发重试后页面同时出现两个 Agent 气泡

用户实机走查 ADR-0018 路径的截图:实验运行 FAILED → 回「按典型参数继续」→ 上面一个 Agent 气泡「已重试阶段 · 实验运行 ✓ / 正在生成回复 / 思考中…」,下面又一个 Agent 气泡「收起执行步骤 / 重试失败阶段。/ 已记录补充要求…(JSON 行)/ ws_list / env_probe / 深度思考 · 实验执行」,两个同时在转;第二张截图里上面那个气泡的思考正文在逐项算分。读码根因:

  1. 渲染:tailTraceHost 只认「尾部是轨迹块才续写,否则另起带署名的新块」。对话即控制面之后,动作由对话轮自己触发,executed 回执 → omm:run-reopened → 控制器重接 SSE,第二次尝试的事件到达时尾部正是还在生成的回复块,于是另起一块。
  2. 内容:prompt_block 的回复要求「先告知已执行的操作,再回答用户的问题」把「按典型参数继续」当成了问题,模型在对话里替运行去算典型参数下的结果——聊天模型与运行里的实验节点并行解同一道题。
  3. 顺带:run.log{kind:"user_note"} 没有专门分支,落进「其他 run.log 原样 JSON」兜底(terminal 图标 + 可展开的原始 JSON,含备注全文与 note_id)。

改动(用户拍板:三项一起做):

  • 前端 integration/modeling-workspace-controller.ts:抽 activityHeader();新增 replyRunTraceHost(replyBlock)(回复块内只建一次的折叠头 + .agent-stream.run-trace);tailTraceHost 加第二条规则「尾部是 .follow-up-reply → 写进该回复块」;user_note 并入「有现成人话 message 的运营事件 → 叙述行」分支。legacy/openmathmodel-ui.ts:appendReplyActions 改为紧跟 .analysis-copy 插入;toggle-activity 优先折叠折叠头紧随其后的列表。workflow-refresh.css revision 38:折叠头贴正文时 4px 上间距。
  • 后端 run_control.py:_HANDOFF_KINDS / reply_rules(actions) 三档回复口径(交接 / 提案待确认 / 默认),prompt_block 末行改用它;导出 reply_rules。tests/test_run_control.py +1(test_reply_rules_hand_off_to_the_run_after_executed_actions:六种交接动作同一口径、提案口径、rejected / dismissed / pause / cancel 与无动作走默认、prompt_block 末行一致)。
  • 不动:受保护入口、路由、DOM 槽位类名、/actions 与轮视图契约(action 事件形状不变)、刷新重进路径(历史事件仍回放到首气泡、对话轮在其后重建)。

当日执行并通过:pytest backend/api/tests/test_run_control.py 29 项;npm run check;npm run build(index 745.58 kB);node --test "src/**/*.test.mjs" 137/137。浏览器验收待用户(本环境无浏览器):失败态回一句让运行继续的话 → 只有一个 Agent 气泡:轨迹行「已重试阶段 · 实验运行」→ 简短交接回复(不再自己算题)→ 复制按钮 → 「收起执行步骤」+ 第二次尝试的步骤在同一气泡里往下走;点折叠头只收起步骤区、回复轨迹行不受影响;「已记录补充要求…」为一行叙述、无 JSON 展开;再发一条消息后新的步骤落到新回复块里。已知但未动:首气泡封口后(题意解析完成)没有对话时,后续阶段的步骤仍按既有设计另起一个「收起执行步骤」轨迹块,与首气泡相邻——属 2026-08-21 起的既有形态,如需一并合并再议。

2026-09-07 报障修复:首页新开对话「延续」了上一个任务(任务附件串台)

用户从任务「机器人竞技攻击策略优化」页回到首页,新开对话只问了一句「今天是什么日子」,Agent 却回「您上传的是 B 题:机器人竞技策略的优化问题,我已经读取并梳理了题目内容」并开始分解问题。

根因不在服务端隔离(chat_… 归属按 user 隔离、不注入任何运行上下文;对话历史也已由 configureConversation 换绑清空),而在前端拼上下文:sendConversationTurn 对任何归属都调用任务附件收集器,而 task-attachment-context.ts 判断「当前任务是谁」不看对话绑定的归属,读的是 URL run_id、没有则退回标签页级的 sessionStorage.openmathmodel.activeRunId——这个键在每次打开任务页时写入、只在任务被删 / 404 时清。首页没有 run_id,于是拿着上一个任务的 run 拉 /workspace 的上传附件,把题面 PDF 全文以「用户为本任务上传了附件,内容如下:【任务附件:…】」并进了新对话的第一条消息。连带一个缓存缺陷:附件清单缓存 entriesPromise 模块级、不按 run 分键,resetTaskAttachmentContext() 定义了但无人调用——同一标签页从任务 A 切到任务 B 不刷新时,B 的对话要么带上 A 的附件、要么(A 的都已注入过时)永远拿不到 B 自己的附件。

  • 前端 attachments/task-attachment-context.ts:collectTaskAttachmentContext(runId) 改为显式接收对话绑定的运行,不再读 URL / sessionStorage 猜身份(删 activeRunId() 与 ACTIVE_RUN_KEY);不是 run_… 直接返回 null 且不发请求;清单缓存记 entriesRunId,run 变了整套状态(清单 / 首轮等待预算 / 摘录标记)自动作废。integration/agent-chat.ts:只在 turnScope 是 run_… 时收集任务附件;configureConversation 换绑时调用 resetTaskAttachmentContext()。新增 attachments/task-attachment-context.test.mjs 3 项(非运行归属不产生附件也不读残留身份;换运行清单作废、切回重新并入;摘录兜底按运行隔离)。
  • 不动:taskGoal() 在无归属(演示态)时读全局 openmathmodelPrompt 的既有兜底——有归属时本就只认 scopeGoal,首页对话不受影响;服务端 CHAT_SYSTEM_PROMPT 未加当前日期(「今天是什么日子」答成 5 月 7 日是模型不知道日期,用户暂不要求改)。

当日执行并通过:npm run check;npm run build(index 751.02 kB);node --test "src/**/*.test.mjs" 145/146——唯一失败项 paper-figures.test.mjs 读的 packages/contracts/fixtures/v1/valid/document-draft.4.json 正被另一条切片线程改动中,与本修复无关。浏览器验收待用户:从任意任务页回首页新开对话问一句无关的话,回复不应提到该任务的附件 / 题面;任务页对话仍能在开场分析与追问里看到自己的附件;同一标签页从任务 A 切到任务 B(不刷新)追问时,B 的回复引用的是 B 的附件。

2026-09-07 报障修复:首页对话的回复没有复制按钮;回复右下角新增赞 / 踩

用户在首页对话里看不到回复右下角的复制按钮,以为是回归。核对代码与 git log:复制按钮(appendReplyActions)自 2026-08-13 起只挂在任务页的回复块上(legacy/openmathmodel-ui.ts),首页对话的回复由 integration/home-chat.ts 渲染,从未挂过操作区——不是丢了,是首页从来没有。顺带按用户要求加评价按钮;用户拍板:赞 + 踩一对、落服务端对话轮、首页与任务页都加。

  • 后端:chat_turns.feedback(String(8) 可空:up / down / NULL)—— ORM + Alembic 0020_chat_turn_feedback(SQLite 开发库由 _add_missing_sqlite_columns 自动补列);ChatTurnHub.set_feedback 与 set_trace 同一套落点(内存里的轮先改内存再回写,否则直接改库行);PUT /api/chat/turns/{id}/feedback(ChatTurnFeedbackRequest{feedback: up|down|null},非法值 422,别人的轮 404);轮视图(内存快照 / 库行)多出 feedback。test_chat_turns.py +3:置值 / 改评 / 撤回三处一致且不碰正文与轨迹;内存缓冲已回收只剩库行的轮也能评;非本人 404。OpenAPI 基线随之重导(+1 路径、+1 schema)。
  • 前端:新增 integration/reply-actions.ts——两页共用的操作区(复制 + 赞 + 踩),紧跟 .analysis-copy 之后插入;纯逻辑(replyActionsMarkup / nextFeedback / createFeedbackController:先按下再落库、失败退回原状并 toast、落库期间的点击忽略、以服务端确认值为准)与 DOM 挂载(mountReplyActions)分开,reply-actions.test.mjs 4 项覆盖前者。没有服务端轮 id 的回复(本机旧记录、无状态通道)只显示复制——评价没有落点就不摆出来。legacy/openmathmodel-ui.ts 的 appendReplyActions 改为委托该模块,四处调用(重建追问 / 重建开场 / 续接直播 / 首发)都把轮 id 与已有评价传进去;home-chat.ts 的 presentReply 收尾与 appendSettledReply 重建同样挂上。chat-turns-api.ts:ChatTurnView.feedback? + setChatTurnFeedback;agent-chat.ts entryFromTurn 的条目带 turnId / feedback(ConversationLogEntry 新增两个可选字段,只由服务端轮映射产生,本机旧记录不带)。styles.css:.reply-action-button[aria-pressed="true"] 按下态(实心图标 + 深色,含暗色主题);en-US.ts +3 词条。
  • 不动:受保护入口、页面结构与路由;.reply-actions 位置与既有复制交互不变。

当日执行并通过:pytest backend/api/tests 427 passed / 2 skipped;export_openapi.py --check 重导后一致;npm run check;npm run build(index 753.18 kB);node --test "src/**/*.test.mjs" 150/150。浏览器验收待用户:首页对话与任务页的每条已完成回复右下角都有「复制 · 赞 · 踩」三颗按钮;点赞后图标实心、刷新 / 重进仍保持;再点一次撤回、点另一个改评价;后端不可达时按钮退回原状并提示「反馈保存失败」。

2026-09-07 报障修复:过程行展开无限高;「一切页任务就失败」的真实原因

用户两条报障:(1)活动流里「深度思考 · 实验执行」等小标题下的盒子展开后高度无限,要和聊天思考块一样限高、内部滚动;(2)明明还在思考中,一退出建模页 / 切别的对话,运行就变成「执行失败」。

(1)是既有设计的有意选择(workflow-refresh.css 原注释「展开详情是用户主动要读全文:内容多长就多高」),按用户要求改掉:.stream-detail pre 统一 max-height: min(260px, 40vh) + overflow: auto(首版取聊天思考块的 380px,用户复验后要求再矮一点,压到约 10 行),细滚动条贴容器底色;生成中的实时区仍是 220px 矮窗吸底。覆盖思考 / 阶段产出 / 沙盒执行 / ws_list·env_probe 原始日志 / 回复轨迹行,前端零逻辑改动。

(2)查库复盘 run_4c9db02321d344c8998ab9a0aa805619,与切页无关——前端切页只关 SSE、中止本页 fetch,服务端 cancel_run / pause_run 只由显式动作触发;真实链路是两件事叠加:

  • 开发者以 uvicorn omm_api.asgi:app --reload … 在 backend/api 起 API 却没加 --reload-dir(README 早已标注「必带」):watchfiles 默认监视 cwd 下全部 *.py,沙盒第一次 python_run 写出 data/workspaces/<run>/steps/<step>/main.py(21:49:45)即触发重载,18 秒后新进程的 heal_interrupted 把在途步骤判成 interrupted: executor lost before completion(TRANSIENT)——页面上就是「深度思考 · 实验执行(本次调用中断)18s」。这种配置下实验阶段每次尝试都会在第一次 python_run 后被打断,永远跑不完。
  • 第 2 次尝试里 DeepSeek 实验模型(deepseek-v4-flash-vision-exp)两次把完整的 {"tool": "python_run", …} 信封写进 reasoning_content、content 为空(同一运行五次沙盒调用有三次如此),chat_text 原样把空串交给内环 → 「输出未通过结构校验」修复梯 → 仍为空 → experiment sandbox failed … 0 run(s): 尚未用 python_run 运行任何代码,运行 FAILED。

改动:

  • 后端 llm.py:新增 salvage_answer_from_reasoning——正文为空且思考以 } 收尾时,从最后一个 } 往前找第一个能把尾巴整体解析成 JSON 对象的 {(容忍末尾 ``` 围栏),_account_and_emit 以之为答案返回并在 `llm_call` 事件打 `answer_from_reasoning: true` 审计标记、日志 warning;正文非空时思考一字不掺。`engine_glue.advance_run`:`heal_interrupted` 真修复到步骤时调用 `_note_executor_restart`——落 `run.log{kind:"executor_restarted", message:"后端进程在「X」执行中途重启,进行中的模型调用被打断;已自动作为第 N 次尝试重跑该阶段"}` 并在 omm.engine 日志给出 `--reload-dir` 修正命令。新增 `tests/test_llm_reasoning_salvage.py` 8 项、`tests/test_executor_restart_note.py` 2 项(用 BaseException 穿透推进链复现悬挂 RUNNING 步骤;断言叙述事件落在 `step.failed` 之后、第 2 次 `step.started` 之前,正常推进零叙述)。
  • 前端 modeling-workspace-controller.ts:executor_restarted 并入「有现成人话 message → 叙述行」分支,不落原始 JSON 兜底。
  • 工具链:新增 tools/dev-api.mjs 与根 npm run dev:api——README「手动双终端」那条命令的封装(--reload-dir backend/api/omm_api --reload-dir agents --timeout-graceful-shutdown 5),附加参数透传 uvicorn;两份 README 的手动命令上方都改为先推荐它。
  • 不动:受保护入口、路由、DOM 槽位;heal_interrupted 语义(仍然失败重跑);沙盒工作区位置。

当日执行并通过:pytest backend/api/tests 435 passed / 2 skipped / 1 deselected(唯一剔除项 test_stage_outputs.py::test_references_projection_… 依赖正被另一条切片线程改动中的 stage_outputs.py,与本修复无关);npm run check;npm run build(index 755.67 kB);node --test "src/**/*.test.mjs" 150/150;npm run dev:api -- --port 8011 实机启动日志 Will watch for changes in these directories: [agents, backend/api/omm_api]、/api/health ok。浏览器验收待用户(本环境无浏览器):任务页展开任意「深度思考 / 阶段产出 / 已在沙箱执行实验代码 / ws_list」行,盒子最高约 260px(约 10 行)、内部出现细滚动条、对话不再被推走;用 npm run dev:api 重启 API 后重跑实验阶段,python_run 之后不再出现「本次调用中断」;若仍以旧命令启动,活动流会在中断行下方补一句「后端进程在「实验运行」执行中途重启…」说明原因。

2026-09-07 报障修复:对话里说「重试」后新进度只在页面顶端;黑色主操作贴着下一条用户气泡

用户截图:在对话里让 Agent 重试(ADR-0018),回复「已重试阶段 · 实验运行」落在底部,但重试后的 ws_list / env_probe / 「深度思考 · 实验执行 1m 8s」全在页面顶端的首气泡里,底部看不到任何动静,得往上翻几屏才找到;另外首气泡末尾的黑色「Agent 正在执行」几乎贴着下一条用户气泡。

根因:实时事件自 2026-08-21 起已经按「首气泡封口后流向对话末尾」排布(tailTraceHost),但重进 / 整页导航(切阶段页、切别的任务再回来)时 hydrateHistory 回放的历史事件一律落回首气泡(resolveStreamHost 的 replay 分支),而对话轮由 omm:conversation-restore 重建在其后——重试是在对话里触发的,用户随后切页几乎是必然的,于是每次回来都是「进度在顶、对话在底」。阶段页左栏(.focused-agent-scroll)更进一步:resolveStreamHost 只认 .chat-scroll,那里连实时事件都一律堆在摘要下方。间距:.user-message 只有 4px 上边距,Agent 块 → 用户气泡之间没有任何规则补齐(反向的用户气泡 → Agent 回复是 24px)。

  • 前端 integration/modeling-workspace-controller.ts:AgentStreamState 新增 replayAtMs(回放中这条事件的服务端时间);新增 historyTraceHost(scroll, atMs)——找最后一条 data-turn-at 不晚于事件时间的用户气泡,事件写进它紧随的回复块内部的运行步骤区(replyRunTraceHost,与实时到达时同一落点),早于全部追问的仍落回首气泡;conversationScroll 同时认 .chat-scroll 与 .focused-agent-scroll;liveEventsFlowToTail:总览页沿用「首气泡封口」判定,阶段页左栏在计划揭示且已有对话时才把实时事件排到末尾(没有对话时保持「摘要 → 步骤 → 主操作」,不为一条步骤另立署名块);tailTraceHost 的署名改从所在 .chat-pane 里克隆(阶段页取面板顶部的 Agent 署名)。legacy/openmathmodel-ui.ts:appendRestoredUserBubble 多收 createdAt,服务端轮重建(含在途轮续接)时写 data-turn-at;本机旧记录无服务端时间,不带标记、仍在所有轮之前。
  • 样式 workflow-refresh.css revision 40:.chat-scroll .assistant-block + .user-message、.focused-agent-scroll .assistant-block + .user-message、.focused-agent-scroll [data-agent-cta] + .user-message 补 24px 上间距,与「用户气泡 → Agent 回复」同一节奏;首页对话线程自带 .assistant-block 下边距,不受影响。
  • 不动:受保护入口、路由、DOM 槽位;实时事件在总览页的既有落点;step.* 仍不进活动流。

当日执行并通过:npm run check;npm run build(index 756.49 kB);node --test "src/**/*.test.mjs" 150/150。浏览器验收待用户(本环境无浏览器):任务页对话里说「重试」→ 切到实验页再切回(或刷新)→ 重试后的 ws_list / env_probe / 深度思考行应出现在「已重试阶段 · 实验运行」那条回复的下方(「收起执行步骤」折叠头之后),页面停在底部即可看到,首气泡里只剩重试之前的轨迹;阶段页左栏有对话时新步骤同样排在最后一条回复之下、没有对话时仍在摘要下方;首气泡黑色主操作与下一条用户气泡之间约 24px。

2026-09-07 报障修复:任务总览页的「前往 X」主操作也在页面顶端,得往上翻才够得着

同一条报障的延伸:首气泡末尾那颗黑色主操作([data-agent-cta],总览页是「前往实验与验证」这类跳转、阶段页左栏是重试 / 确认方案)随首气泡一起被对话推到几屏之上。第一版把它镜像成一枚小药丸放进输入框上方「执行计划」面板的头部,用户否决(位置不好看)——要的是和执行进度一样,跟到最新一条对话的最下面。镜像方案已整体撤回,改为搬按钮本体:

  • 前端 integration/modeling-workspace-controller.ts:新增 followConversationTail(root)——对话区里最后一条 .assistant-block.follow-up-reply(回复块或轨迹块)存在时,把审批选项列表(renderApprovalOptions 摆在按钮前面的 [data-approval-options])与按钮一起 append 到它的末尾;没有对话时退回原位。搬的是按钮本体:文案、禁用态、onClick 处理与幂等 token 全部沿用,不存在两处分叉。原位留一个隐藏占位 [data-agent-cta-home](放在按钮之后而不是之前——renderApprovalOptions 靠 cta.previousElementSibling 找已有选项,前面多个占位会让它每次刷新再建一份),块被移除时按钮退回占位、绝不随块消失。触发点三处:renderAgent(每次快照刷新,选项列表刚摆好一起搬)、streamAppend / appendGrouped(replyRunTraceHost 在尾部回复块末尾新建步骤区会排到按钮之后,追加后把按钮重新压到最后)、对话区直接子节点的 MutationObserver(用户新发一轮由页面层直接写进对话区,控制器没有回调可接;只看直接子节点,按钮搬进块内是孙节点变化不会触发自己;cleanup 里 disconnect)。演示夹具([data-demo-only])里的 CTA 不动。
  • 样式 workflow-refresh.css revision 41:.follow-up-reply > .running-live-cta / .focused-stage-cta 12px 上间距;紧跟 .agent-stream(自带 10px 下边距)时 2px、紧跟 .approval-options(自带 12px)时 0,与它在首气泡里的间距一致。revision 40 的「Agent 块 → 用户气泡 24px」补一条 [data-agent-cta-home] + .user-message(阶段页左栏按钮搬走后占位顶替它的位置)。
  • 撤回:task-todo-panel.ts 的 .todo-head-row / .todo-action / renderTaskTodoAction 与对应样式、控制器里的镜像同步与 [data-todo-action] 点击分支——面板恢复原样。
  • 不动:按钮的动作语义、审批选项列表的渲染与滚入视野、受保护入口与 DOM 槽位。

当日执行并通过:npm run check;npm run build(index 757.40 kB);node --test "src/**/*.test.mjs" 150/150。浏览器验收待用户(本环境无浏览器):任务总览页对话拉长后,黑色「前往实验与验证」应出现在最后一条 Agent 回复的最下方(复制 / 赞 / 踩那一行或「收起执行步骤」步骤区之后),再发一条消息它跟到新回复的下方,首气泡末尾不再有按钮;阶段页左栏同理(失败态「重试当前阶段」、待确认「确认 Agent 当前方案并继续」及其选项列表一起在底部);没有任何对话时按钮仍在原位;点击效果与原先一致。

2026-09-08 报障修复:问失败原因每次都以「本轮我没有执行任何运行控制动作…如实说:」开头;crash loop 烧光节点预算

用户连问三次「为什么失败」,每条回复都先来一句「本轮我没有执行任何运行控制动作,所以不会假装“刚才做了重试”。如实说:上一轮你让我“重试”,但系统实际并没有跑通…」。两件事:

  1. 开场白是提示词泄漏。 run_control.prompt_block 在没有动作时写「- 无(本轮没有执行任何运行控制动作;不要声称做了)」,回复要求又统一是「先用一两句话如实告知上面已执行的操作…不得声称…」——没有动作时模型便逐字汇报「没有执行操作」,再把「如实」「不要假装」这些道德化字眼原样搬进正文。用户看不到系统提示,只觉得 Agent 在自说自话。
  2. 为什么总失败:查库 run_4c9db02321d344c8998ab9a0aa805619 的步骤表,实验阶段第 3–25 次尝试全部 interrupted: executor lost before completion(23:18–00:00,每趟约一分钟)——API 进程仍是 20:49 那个没加 --reload-dir 的 uvicorn --reload(见 2026-09-07 条目),每趟第一次 python_run 写出 main.py 就被重载杀掉,heal_interrupted 落定后引擎自动以 attempt+1 重跑,无上限循环,直到 EXPERIMENTING 节点 39 万 tokens / 39 次调用 / 20 次沙箱撞上 300k 的节点预算([E320],POLICY_BLOCK)。此后连人工重试也会在预算预检处立刻被拦——账本按整个运行累计,这正是模型回复里说的「即使我发出重试指令也会被直接拦截」。
  • 后端 run_control.py:回复口径拆成四条并去掉「如实 / 假装」措辞:_REPLY_RULES_PLAIN(本轮没有任何动作——直接回答问题,开头不要先声明本轮没有执行什么操作)、_REPLY_RULES_REPORT(有被拒绝 / 放弃 / 暂停取消类动作——先交代动作结果再回答)、_REPLY_RULES_HANDOFF / _REPLY_RULES_PROPOSED 措辞同步;四条末尾统一加「这些要求是给你的内部约束,不是回复内容:不要提及、复述或解释它们」。状态块标题改为「【本轮运行控制动作】」,逐条标「已执行 / 待用户确认 / 未执行(当前状态不允许)/ 已放弃」,没有动作只写「- 无(这一轮是普通问答,运行状态没有变化)」。test_run_control.py 三处断言随之更新(含端到端的系统提示词检查)。
  • 后端 agents/core engine.py:新增 INTERRUPTED_STEP_ERROR 常量与 TaskRunEngine.fail_run(snapshot, error)(在两步之间从工作态直接落 RUN_FAILED,终态 / 评审门不动)。engine_glue.advance_run:heal_interrupted 之后数当前阶段末尾连续 executor-lost 的步骤数(_interrupt_streak),达到 MAX_CONSECUTIVE_INTERRUPTS = 3 就 fail_run 而不是再开一趟——失败信息写明「连续 N 次被后端进程重启打断,已停止自动重跑以免空耗模型额度」及 --reload-dir / npm run dev:api 的出路,含 executor lost 标记 → TRANSIENT,UI 照常给「重试当前阶段」;人工重试只给一次新尝试,环境没修好再被打断立刻再次停下。test_executor_restart_note.py +1(三次打断后停下、预算不再被烧;重试一次再被打断再停;节点恢复后重试跑通)。
  • 不动:预算治理的额度与语义(本次运行的 EXPERIMENTING 节点已超 300k,重试仍会被 E320 拦下——需启动 API 前设 OMM_NODE_MAX_TOKENS 提高上限,或新建任务);对话控制面的动作判定与执行。

当日执行并通过:pytest backend/api/tests 440 passed / 2 skipped / 1 deselected(同一条被另一会话改动中的 stage_outputs 用例);pytest agents/core backend/worker 169 passed。浏览器验收待用户:在失败的任务里问「为什么失败」,回复应直接从原因说起,不再有「本轮我没有执行任何运行控制动作 / 如实说」开场;让 API 反复重启的环境下,实验阶段最多连续 3 趟被打断即停在 FAILED,失败信息里能看到 --reload-dir 的提示。

2026-09-08 拍板:四项资源预算默认取消上限

上一条的收尾是「不动:预算治理的额度与语义」,用户看到实验阶段又被 [E320] 拦下后拍板推翻——默认不要上限。这次改的是默认值与"无上限"这件事能不能被表达,不是把闸门删掉。

改的理由(一句话:保护弱、副作用重):额度原本挡的失控是「进程反复重启 → 阶段无上限重跑」,那条已由上一条的 MAX_CONSECUTIVE_INTERRUPTS = 3 在源头掐断,不再需要拿钱包当刹车;而额度一旦烧光是整个运行不可逆地卡死——账本按 run 累计、预检在调用之前,人工点「重试当前阶段」也会立刻被拦,用户只能弃掉这个 run 重开(2026-09-07 的 run_4c9db023… 正是如此)。

  • harness budget.py:新增一等值 UNLIMITED(= math.inf)与 is_unlimited(),四个维度(run tokens / 调用次数 / 沙箱次数 / 墙钟)以及节点 tokens 在无上限时跳过越线检查,账本照记——报告与错误上下文里的用量始终是真数。取 inf 而不是 -1 之类的负数哨兵:下游有 budgets.max_sandbox_runs < 1(实验节点判断"够不够复跑核对")、min(6, budgets.max_sandbox_runs)、thread.join(max_wall_clock_s) 这类裸比较,负数会被读成"一次都不给"而静默跳过复跑,inf 则让每处比较天然成立。0 保持原义(真实额度"一次都不给",subagent_slice 在父预算耗尽时就切出 0,下游靠它跳过工作),没有被并进"无上限"。
  • harness 同文件:BudgetGovernor 增 default_node_budget。此前 check_llm_call 对没 open_node 过的节点回落到 NodeBudget() 即 §4.7 的 30 万,执行面只 open _PROMPT_NODE_IDS 里的节点,任何表外节点都会绕过本地配置在 30 万处被硬停。subagent_slice 逐维度传递无上限(四分之一的无穷还是无穷);snapshot()["limits"] 与 subagents.py 的 spawn 审计把非有限值报成 null——两处都会进 JSON / jsonb 列,json.dumps(inf) 出来的裸 Infinity 会让整条事件被拒收。RunBudget / NodeBudget 的字段类型随之改成 int | float,§4.7 那张表的数字原样保留(设计契约不动,由执行面决定传什么)。
  • 执行面 engine_glue.py:_env_int → _env_limit,默认返回 UNLIMITED,只有正整数才恢复硬闸(0 / 负数 / 非数字 / 不填都是关闭——手敲环境变量的人写 0 想表达的是"别限",与治理器里的 0 语义不同,注释已写明);_scaled_limit 让修订轮追加配额对无上限维度成为空操作;_build_budget_governor 同时把节点预算传给 open_node 与 default_node_budget。_budget_stop_message 改口径:默认不设限,出现这条说明本机显式配了额度,指引改为"调高或删除对应环境变量(置 0 即关闭)后重启 API"。
  • 文档:backend/api/README.md 环境变量表补四行 + 一段说明(含"用量监控的月度费用预算与硬限制是另一套闸门,仍然生效");.env.example 补一组注释掉的示例值。
  • 不动:LoopBudget 的 max_turns / repairs / no_progress_k / tool_fail_m(E330–E332)与 MAX_CONSECUTIVE_INTERRUPTS、MAX_REVISION_ROUNDS、子代理并发上限——这些是进展护栏不是钱包护栏,去掉就真的没有停机条件了;设置中心「用量监控」的月度费用预算与硬限制(usage.py)也不动。

当日执行并通过:pytest backend/api/tests 444 passed / 2 skipped;pytest agents 618 passed。新增用例:harness 5 条(无上限不停但照记账 / 表外节点用调用方给的回落值 / 0 仍是真实额度 / 切片保持无上限且不误触发 <1 / limits 报 null 且 json.dumps(allow_nan=False) 不炸),后端 3 条(不配环境变量四项全 inf、0 即关闭、正整数恢复并按轮追加;默认治理器放行 80 万 tokens / 80 次调用 / 50 次沙箱且含表外节点)。test_run_revisions.py::test_each_round_appends_one_more_quota 补 setenv——默认无上限下 inf * 3 == inf 会让原断言恒真、守卫失效。改动只在 API 进程内生效,用户需重启后端;已卡在 [E320] 的历史运行重启后点「重试当前阶段」即可继续(账本仍在,但不再有线可撞)。

2026-09-08 报障修复:模型还在思考、运行就已重启——对话控制动作改为回复结束后执行(ADR-0020)

用户带图报障:在失败的任务里发了一句带「重试」的话,模型还在「思考中」,回复块下方的执行步骤区(ws_list / env_probe / 深度思考·实验执行)就已经出现并开始滚动,回复随后才说「已按你的指令重试当前阶段」。拍板两条:「模型回答完了之后,再出现下面这些执行的」;「有关键字了就直接触发重新执行肯定不行,要结合在一起的」。查明是两处设计叠加:routers/chat.py 的 producer 在调用模型之前就跑 run_control_step(判定 + 立刻 execute_action),运行一变 RUNNING 工作台就重接事件流、在尾部回复块里长执行步骤;run_control.decide 又把规范化后 ≤12 字的短句词表命中直判、长句判定模型说 none 时回落词表结果——两条路都是「有关键字就执行」。

  • 后端 run_control.py:run_control_step 拆成 plan_control_step(回复前:读状态 + 判定 + 生成将来时的状态块,不改状态、不写库、不发事件)与 execute_control_plan(回复后:重读运行状态复核合法性 → 执行 / 落备注 → 回执)。复核失败(回复期间用户点了按钮、待审批门换了)回 action{status:"rejected", code:"RUN_STATE_CHANGED"},不按过期快照硬执行。新增 ControlPlan / describe_plan(将来时安排,与 execute 回执一一对应)/ execute_dry(提案与放弃这两条不碰库的分支)。decide:删掉 RULE_FAST_PATH_MAX_CHARS 短句直判与「模型 none 回落规则」,词表候选作为「参考信号」一行随判定提示词下发(明写「词表匹配不等于用户在下命令」);仍本地直判的只有对上一轮提案的「确认 / 算了」。Judge 签名加第四参 hint。reply_rules 四档口径全改将来时并禁止「已重试」「已选择」;prompt_block 加 note_planned(不触发动作但会落备注的一轮,说明「只是记录,不是修改」)。
  • 后端 chat_turns.py:ChatTurnHub.start 增 before_finish 钩子;_run 重构为「所有终态路径(上游 done / error、静默收尾、事件超限、线程异常)汇到 _finish」——钩子在锁外跑、只在轮仍 running 时调用,产出的 action 事件排在终态之前并记进 meta.actions;钩子跑完若观众恰好 stop 了,动作已经发生,回执照样进 meta(刷新可见)、只不再改终态。用户 stop 后生成线程按原样 break、terminal 为 None、钩子不跑——被打断的轮不执行任何动作。
  • 后端 routers/chat.py:producer 只做计划并注入提示词,before_finish 里 execute_control_plan → action_events。
  • 前端 openmathmodel-ui.ts:回复呈现器的 onAction 不再插到生成行之前,改为追加在末尾,并攒进 tailRows;settleGeneratingRow 增第五参,在「已生成回复」落定之后再把它们接进 traceLog(轨迹日志顺序与页面一致,重建不会把回执排到生成行前面)。agent-chat.ts 注释随语义更新。run-control-view.ts 纯函数不变。
  • 文档:新增 ADR-0020;ADR-0018 / ADR-0019 状态行标注被修订的条目。
  • 不动:/actions、闸门、引擎状态矩阵;判定模型仍是池里最弱的一个、按 route 记账;受保护入口与 DOM 槽位。

当日执行并通过:pytest backend/api/tests 453 passed / 2 skipped;npm run check;node --test "src/**/*.test.mjs" 155/155。test_run_control.py 重写为 35 条:事件顺序断言从「首条是 action」改为「全部 delta → action → 终态」(_assert_actions_after_reply),mock 判定器按「用户这句话」查表作答(词表不再直判,每句都出网);新增 5 条——对话 SSE 正文两个分块之间探运行状态仍是 FAILED、判定说 none 时「重试」「继续啊」「再跑一次」都只是对话、慢流中 stop 后运行不动 / 不落备注 / 无回执且再说一次即正常执行、回复期间点了按钮则回执 RUN_STATE_CHANGED、回复模型 500 时动作照样执行且回执排在 error 之前。后端需重启。浏览器验收待用户(本环境无浏览器):失败任务里说「重试」→ 回复先逐字说完(内容应是「接下来会重试…」而非「已重试」)→ 回复下方出现「已重试阶段 · 实验运行」一行 → 执行步骤区才开始滚动;回复期间按暂停键 → 运行保持失败、不出现回执;「重试过几次了?」这类问句不触发动作。

2026-09-07 复验修复:建模流程页文字穿盒 / 拥挤;论文正文逐字流式;论文大纲栏

用户带图复验三条:(1)建模流程页仍有「字穿过盒子」与「非常拥挤」;(2)论文页右侧正文要一个字一个字出现,不能直接整篇出现;(3)论文页左侧目录栏难看。用真实产出(run_4c9db02321d344c8998ab9a0aa805619 的三案方案、两篇万字终稿)经 Playwright 路由拦截灌进测试运行复现,1280–1920 四档视口逐一探测横向溢出。

  • 穿盒的真实原因在论文纸面:.editor-page 是 flex 列容器里 margin: 0 auto 的项,不被拉伸、宽度按内容 min-content 算——朱诺论文里一条宽公式(.md-math-block min-content 753px)把纸面撑到 917px 而容器只有 823px,右侧正文被外层 overflow: hidden 切掉。修:.paper-only-editor .editor-page { width: 100% }(1060 上限与居中不变),公式在自己的块里横向滚动;多列数字表(min-content 724px)在 stage-content.ts 里套 .md-table-wrap(overflow-x: auto)自行滚动,纸面不再出横向滚动条。
  • 拥挤在方案行:.focused-plan-row 原是六列一行(标题 205px 定宽 + 三段摘要按比例分),真实产出的核心方法与主要风险都是长句,摘要列三行截断把行高撑得参差、1366 宽下省略号里夹半个字。改成两行卡片:第一行 radio / 标题 / 核心方法(弹性列、单行省略、悬停全名)/ 步骤数,第二行整行主要风险(首句上限 48→90 字,单行省略、悬停全部风险);卡片高度恒定,1280–1920 全档不再拥挤;≤820px 手机档沿用 revision 32 逐行堆叠(grid-row: auto 复位)。方案详情表标签列 128→136px、行内距 15→18px、列表项间距 6→8px;左栏审批选项卡说明字号 12→12.5px / 行距 1.65、长标识符允许折行。顺带修 agents/skills nodes.py::_plan_blurb:首句自带的「;」再接「;实现语言」出现「;;」,先去掉首句句读。
  • 逐字流式没生效的原因:renderEditorPanel / appendPaperSection 都以 editor.offsetParent !== null 为动画前提;合并工作台里五个阶段面板同存、论文面板在用户停留于别的阶段时是隐藏的,正文(分章事件与终稿)恰恰都在那时到达,于是整段直落并把版本记成「已播过」,用户切回论文页永远是「直接出现」。旧实现另有 16ms setInterval + max(3, 总字数/500) 字/帧,万字论文 8 秒播完,看起来是整段整段蹦。重写为 PaperTypewriter(每台编辑器一台,分章直播与终稿共用一条队列):requestAnimationFrame 驱动、按时间折算字数预算(帧率无关,后台标签切回单帧最多补 100ms);速度随积压自适应(积压 / 24,夹在 48–320 字/秒:一万五千字终稿约一分半,单章半分钟上下,尾段逐字可辨);编辑器不可见时暂停(200ms 探一次可见性),切回论文页才从停下的位置继续、不追赶不重播;多章按到达顺序排队,SSE 重放的历史章节若排在正在打字的章节之后轮到时整段直落只保顺序;终稿在直播未打完时到达 → 头部(标题 / 摘要 / 关键词)与已看过的字数直接放出、从直播停下的位置接着打(skipChars = headChars + revealed),直播已全部打完 → 静默换成终稿(原行为);beforeinput 整段放行、stream-in 入场、光标、吸底跟随、reduceMotion 跳过、用户草稿主权、每 30 帧刷新字数统计均保留。大纲链接改为使用时现取(分章直播会按需补挂链接)。
  • 大纲栏:列宽 190→212px(≤700px 容器 160→172px);首版做成「状态点 + 标题」(已写完绿底勾、当前章节左侧主色粗标 + 加粗),用户复验明确不要——改为一列干净的目录:状态点留在 DOM(直播打勾逻辑不动)但 display: none,不画左标、不加粗,当前章节只用浅底(字重 500),未写完的章节用淡一档字色(a:has(.outline-status:not(.done)))示意进度;字号 12.5 / 行高 1.5、overflow-wrap: anywhere;.focused-stage-pane 令牌层与暗色主题同步。
  • 不动:受保护入口、页面模板与路由、DOM 槽位(方案行仍是 radio / strong / 三个 span / i 的既有结构,CSS 按序摆位)。

当日执行并通过:agents/skills pytest tests/test_nodes.py 161 passed;npm run check;npm run build(index 755.67 kB);node --test "src/**/*.test.mjs" 150/150;Playwright 实机:方案页 1280 / 1366 / 1440 / 1536 / 1920 五档探针「scrollWidth > clientWidth」元素 0 个;论文页纸面 scrollWidth == clientWidth;打字机脚本——终稿在论文面板隐藏时到达 4.5s 后正文 0 字、data-streaming=true,切到论文页 0.8s 后 356 字 / 3.3s 后 1563 字,切走 2.5s 内字数不增长,切回继续,键入一字后整段放行、data-streaming 撤销、大纲 8 项全部打勾。截图存 audit-current/stage-polish-2026-09-07/(*-before* / *-after* / typewriter-*.png)。浏览器验收待用户:方案页方案卡两行(标题 + 核心方法 + 步骤数 / 主要风险)不再拥挤;论文页右侧正文不再被右边裁掉;停留在别的阶段等论文写完再切到论文页,正文应从头逐字打出,中途切走再切回从原处继续,开始编辑立即全文放出;左侧大纲是一列纯文字目录(无勾、无左标、不加粗),当前章节浅底、未写完的章节淡灰。

2026-09-17 报障修复:主操作按钮总在对话最底下、无事可做时也显示;「收起执行步骤」折叠头整体撤下

用户带图报障两条:(1).running-live-cta(「重试当前阶段」这颗黑色主操作)总出现在内容最底层、一直显示——问一句「重新开始吧」「为什么失败」这类与运行无关的话,回复下面也顶着它;要求「只有该显示的时候才显示,和它没关系的内容下面不该出现」。(2).activity-summary(「收起执行步骤」折叠头)删掉。

根因:2026-09-07 的 followConversationTail 把按钮搬到对话里最后一条 Agent 消息的末尾,判据只看「是不是 .follow-up-reply」——普通问答的回复块也算,于是每发一条消息按钮就跟到新回复下面;renderAgent 又只在规划期隐藏按钮,运行中 kind:"none" 时照样摆一颗禁用的「Agent 正在执行」占位。折叠头则是 2026-09-07 修「两个 Agent 气泡」时随 replyRunTraceHost / tailTraceHost 重新挂回来的(对话层自己的过程区早在 09-05 就按用户要求撤了)。

  • 前端 integration/modeling-workspace-controller.ts:新增 runTraceBlocks(scroll)——对话里承载过运行事件的 Agent 块(直接子节点带 .agent-stream.run-trace),tailTraceHost 新建的轨迹块活动流也标上 run-trace,成为「这个块与运行有关」的唯一标记。followConversationTail 改为只跟这些块里最后一个:「「X」阶段失败。」写在哪个块,「重试当前阶段」就紧挨着出现在那个块的末尾,之后的普通对话不再带走它;没有这样的块时按钮留在原位(首气泡末尾 / 阶段页左栏摘要之后)。renderAgent:kind === "none"(运行中「Agent 正在执行」、排队「等待任务开始」、已取消「任务已结束」)时按钮与选项列表一起 hidden,不再摆禁用占位;点击回调的 finally 同步这一条(动作生效、运行重新转起来时按钮随之收起)。删除 activityHeader(),replyRunTraceHost / tailTraceHost 不再挂折叠头。
  • 前端 legacy/openmathmodel-ui.ts:?demo=1 的两处演示模板(总览页首气泡、阶段页左栏时间线)去掉 .activity-summary 按钮;toggle-activity 点击分支(含 .steps-count 徽标重建)整段删除;注释同步。i18n/en-US.ts 删「查看执行步骤 / 收起执行步骤」两条死词条。
  • 样式 styles.css / workflow-refresh.css:删除全部 .activity-summary 规则(基础 / 暗色 / .modeling-chat-pane / .focused-agent-scroll 三档响应式 / [hidden] / hover·active)、.steps-count 与 omm-count-pop、只由折叠头写入的 .agent-stream.collapsed 与 api 布局的 .activity-list.collapsed;revision 38 注释改述。.follow-up-reply > .agent-stream + .running-live-cta 等间距规则不动。
  • 不动:按钮的动作语义、审批选项列表的渲染与滚入视野、执行轨迹的落点规则(resolveStreamHost / historyTraceHost)、受保护入口与 DOM 槽位。

当日执行并通过:npm run check;npm run build(index 795.23 kB);node --test "src/**/*.test.mjs" 173/173;真实 Chromium(CDP)里按控制器同一套 DOM 结构跑按钮落位场景 9/9(无对话留原位 / 普通回复不吸走 / 落了运行事件的回复块接走 / 之后的普通回复不再带走 / 新轨迹块接走 / 选项列表同行 / 对话清空退回占位 / 页面无 .activity-summary)。应用内浏览器验收待用户(本环境没有已登录的运行):失败任务里问「为什么失败」→ 回复下方没有按钮,「重试当前阶段」仍留在「「X」阶段失败。」那条叙述所在的 Agent 块末尾;运行正常执行时页面上没有「Agent 正在执行」的灰色按钮;阶段失败 / 等待确认时按钮(与选项列表)出现在失败 / 等待叙述的下方;全站不再有「收起执行步骤」折叠头,步骤行直接展示、每行仍可点开详情。

2026-09-17 报障修复:每次点「重试当前阶段」都得翻到顶上才看得清

用户反馈:每次点重试都要往上滚才能看清发生了什么。读码定位不是滚动条没跟上,而是反馈落点错了——在底部点按钮,重试的进度与失败原因却被写回页面顶端。四个根因:

  1. isPlanningPhase(view) 只看快照(RUNNING + 六阶段全 PENDING/RUNNING),分不出「首次规划」与「第 N 次重试题意解析」:重试把 planPhase 翻回 planning,首气泡被「解封」,resolveStreamHost 把「重试失败阶段。」「深度思考 · 题意解析」写回首气泡摘要下方的活动流;同时 renderCopy 清空摘要、执行计划面板退回「正在思考」。
  2. SSE 帧先入活动流、快照 80ms 后才刷新(onWorkspaceEvent → ingestStreamEvent → scheduleRefresh),落点用的是上一状态的 planPhase:step.failed 与 run.status_changed(FAILED) 相隔不到 80ms 就一起落顶,隔开了就一顶一尾,同一次运行里两次失败叙述位置不一。
  3. 失败原因只存在于顶部:RUN_FAILED 只把 error 写进 run.failure_message,事件 payload(step.failed 的 {node, attempt, failure_class}、状态迁移的 {from, to, reason})都不带文案;时间线上只有一句「「X」阶段失败。」,原因只经快照进首气泡摘要。
  4. 点击之后没有视口跟随;streamAppend 的吸底只在距底 120px 内生效。

拍板(决策卡):A+B+C 全做;E 合并进首气泡。

  • 后端 engine_glue.py:_project_status 增可选 extra,随 payload 一并下发(v1 契约 payload 为自由对象、消费方容忍未知字段);RUN_FAILED 的 FAILED 迁移带 message(= failure_message)与 failure_class。test_task_runs_and_actions.py::test_injected_failure_then_retry_completes 补断言:events/history 里的 FAILED 迁移事件 reason == "「实验运行」阶段失败"、message 与快照 failure.message 一致、failure_class == CODE_DEFECT。
  • 前端 A(规划期单向) modeling-workspace-controller.ts:AgentStreamState 新增 firstStageSettled,由事件流维护(markFirstStageSettled:任何 step.succeeded / step.failed / approval.requested、运行离开 QUEUED/RUNNING、从 FAILED/PAUSED/WAITING_APPROVAL/COMPLETED 回到 RUNNING),实时与首连回放同一判据,且在解析落点之前更新——失败叙述本身就按封口后的规则落位,不再受 80ms 刷新时序影响。isPlanningPhase(root, view) 见到该位直接返回 false;新增 planRevealed(root)(planPhase === "revealed" || firstStageSettled)供 firstAssistantMessage 与阶段页左栏的 liveEventsFlowToTail 共用。挂载时若壳层被复用给另一个运行(root.dataset.runId 不同)丢弃上一个运行的活动流状态。
  • 前端 B(原因随失败落位):run.status_changed{to: FAILED} 改为 streamFailure——叙述行「「X」阶段失败。」+ 直接可见(不折叠)的原因段落 .stream-note.stream-failure(message 原文、white-space: pre-wrap),failure_class === TRANSIENT 时补一句「多为模型接口或网络的瞬态问题,直接重试通常能过。」(其余类别是保守兜底归类,不下结论)。按钮随即由 followConversationTail 压在它下面——原因与动作同处。历史回放从事件重建,刷新后原样。
  • 前端 C(视口跟随本次动作):新增 armFollowTail / disarmFollowTail / shouldStickToTail。按钮的重试 / 恢复 / 确认被服务端受理后(act 成功 → refresh 之后):先把对话末尾滚进视野(按钮可能停在页面中段——失败块之后又聊过几句),之后每条新行都吸底、不看 120px 阈值;用户 wheel / touchstart / pointerdown 即放手;本次尝试落定(状态离开 RUNNING/QUEUED)也放手。appendGrouped 同步改用 shouldStickToTail。暂停不跟随。
  • 前端 E(合并相邻块):tailTraceHost 在尾部就是首条 Agent 消息(还没有任何对话)时返回 null,resolveStreamHost 回落到首气泡摘要下方的活动流——不再另起紧挨首气泡的「Agent」轨迹块(2026-08-21 起的既有形态退役:两个相邻署名中间什么都没有,且与刷新后回放把这批行放回首气泡的形态对不上)。有对话时规则不变:尾部回复块内部的 .run-trace / 尾部用户消息后的轨迹块。
  • 样式 workflow-refresh.css:.stream-failure 左侧 2px 淡红竖线 + 浅底,暗色主题配色;en-US.ts 补瞬态提示词条。
  • 不动:按钮的动作语义、审批选项列表、历史回放的按轮落位(historyTraceHost)、受保护入口与 DOM 槽位。

当日执行并通过:pytest backend/api/tests 462 passed / 2 skipped;npm run check;npm run build(index 802.93 kB);node --test "src/**/*.test.mjs" 173/173;真实 Chromium(CDP)按控制器同一套函数跑落点 / 原因 / 跟随 / 回放场景 20/20(规划期行落首气泡、刷新前到达的首次失败仍封口并留在首气泡且不另起相邻块、重试快照不再翻回 planning、重试行按时间顺序接在失败之后、有对话时失败与按钮一起落到尾部回复块、瞬态失败带提示 / 非瞬态不带、arm 后从顶部滚到底且新行持续吸底、wheel 放手、落定放手、无对话回放全部落首气泡且从历史推出首阶段已落定、重进中途重试的快照不再是 planning)。后端需重启才会在新失败上带原因;已有的历史失败事件没有 message,时间线上仍只显示一句叙述。应用内浏览器验收待用户:失败任务里点「重试当前阶段」→ 视线不离开点击处:按钮下方出现「重试失败阶段。」→ 深度思考走秒 → 「「X」阶段失败。」+ 原因段落 + 按钮重新出现;顶部摘要与执行计划面板在重试期间不再清空 / 退回思考态;没聊过天的运行不再出现两个相邻的 Agent 署名。

2026-09-17 报障修复:论文生成完成后,左侧「论文大纲」仍是「将在论文生成后显示」

用户带图报障:论文页正文一万六千字、底部状态「已恢复本机草稿」,左侧大纲栏却还是模板空态一行字。

根因:大纲只在 renderEditorPanel 把 Agent 定稿灌进编辑器的那一条路上顺手建(buildDraftBlocks → rebuildPaperOutline),编辑器内容另有来源时没人管它。截图正是其中最常见的一种——本机草稿恢复:bindPaperEditor(全局键 openmathmodelPaperDraft.v1)与 task-autosave.restorePaper(按项目键、user_edited)把上次的现场写回编辑器,随后 renderEditorPanel 因 hasUserPaperDraft() 按「用户主权」早退——正文在(多半就是上次渲染出来的定稿,点过一次撤销 / 加粗就算「用户编辑过」)、目录却停在骨架。用户自己增删改标题、论文阶段未产出而用户先写了,同样没有大纲。

  • 前端 integration/stage-content.ts:新增 syncPaperOutlineFromEditor(root = document)——以编辑器里实际存在的非空 h2 为准重建大纲,口径与 buildDraftBlocks 一致(h1 论文标题不进目录;「摘要」与各章是 h2;小节按 paper_section 提示词约定是 ### → h3,不进目录)。没有 id 的标题补 section-N 锚点(已被占用时退到 section-N-2…,绝不改动已有锚点,与定稿渲染的 #section-N / #section-abstract 一致);条目全部标 done(内容都在);spyOutline(从 bindOutlineNavigation 的滚动跟随里抽出)按当前滚动位置高亮。rebuildPaperOutline 记 data-outline-signature,标题集合没变时不重建(保住 active 态)。让路规则 outlineOwnedByStream:分章直播未写完(livePaperState.rendered.size < total,大纲由它预挂全章、逐章打勾)或打字机还在逐字上屏(pending > 0)时不动大纲。正文里一个 h2 都没有:模板空态原样保留;只有残留旧链接时换成「正文中还没有章节标题。」。bindOutlineSync:编辑器 input 300ms 防抖后同步——改章标题、加一章、删一章大纲都跟着变,每台编辑器只挂一次。三处接入:renderEditorPanel 的用户草稿早退分支改为先同步大纲再返回;正常渲染路径挂 bindOutlineSync;renderStageContent 在 document_draft 为 null 时也同步(论文阶段未产出、用户先写了)。
  • 前端 legacy/openmathmodel-ui.ts:render() 末尾(mountTaskAutosave 之后——两条草稿恢复路径都已跑完)非演示态调一次 syncPaperOutlineFromEditor();演示夹具的大纲是固定样张,不动。en-US.ts 补一条词条。
  • 不动:buildDraftBlocks / preparePaperOutline / appendPaperSection 的建纲逻辑与直播打勾;.outline 的 DOM 结构(.outline-heading + a > .outline-status + 文本)与样式;受保护入口。

当日执行并通过:npm run check;npm run build(index 823.11 kB);node --test "src/**/*.test.mjs" 178/178;真实 Chromium(CDP)按同一套函数跑 16/16:只有占位段时模板空态不动;恢复的草稿(h1 + 摘要 h2 + 三章 h2 + 小节 h3)→ 大纲四项、h3 不入、空态移除、全部 done、首项 active;同一批标题不重建;滚动后 active 跟随;用户新加无 id 的 h2 → 防抖后入纲并补锚点;中间插章保持文档顺序、序号被占时退到带后缀的锚点;改标题文字大纲同步;直播进行中 / 打字机未打完 → 让路;删光 h2 → 旧链接换成一句说明、加回来大纲回来;点击大纲滚到对应标题并高亮。应用内浏览器验收待用户:打开截图里那个任务的论文页,左侧应列出摘要与各章目录,点击可跳转、滚动正文时高亮跟随;在正文里新加一个「标题 2」段落约 0.3 秒后出现在大纲里。

2026-09-18 删除深色主题的全部残留代码

2026-08-12 下线主题切换时,html[data-theme="dark"] 规则作为不可达死代码整批保留(见上文同日条目)。业主明确不再需要深色主题,本轮把这些规则连同相关注释和死组件一并删除,界面只剩浅色一套。

  • CSS 规则:用 PostCSS 按选择器逐条删除 355 条规则——styles.css 179 条、workflow-refresh.css 132 条、reference-theme.css 20 条、attachments.css 16 条、projects.css 7 条、accessibility.css 1 条(html[data-contrast="high"][data-theme="dark"])。三处「浅色选择器 + 深色重复项」的选择器列表(.recent、.reference-picker-list、.composer-attachments-head:hover)只摘掉深色那一半,浅色规则原样保留。html[data-theme="dark"] { … color-scheme: dark } 变量块随之消失;:root 的 color-scheme: light 保留。
  • reference-theme.css 的浅色守卫:63 条 html:not([data-theme="dark"]) … 改写为 html:root …。两者匹配同一个元素,特异度同为 (0,1,1),这批调色板规则相对 styles.css 组件默认值的层叠关系分毫未变——直接降成 html 会掉一个类级权重,部分规则会被 styles.css 的双类选择器反超。文件头注释记了这条约束。
  • 死组件:styles.css 的 .theme-options / .theme-option / .theme-preview(含 .theme-preview.dark)整块删除——主题选择器 2026-08-12 已从设置中心移除,DOM 里不再有这些节点;reference-theme.css 中对应的三条覆盖与两处 :is() 列表项同步清理。
  • TypeScript:renderCostChart 的 dataset.theme === "dark" 分支按浅色取值压平。i18n/locale.ts 里「语言与主题共用」的注释、task-routes.test.mjs 里拿 theme: "dark" 当无关键的夹具(改用 interfaceLocale)一并清掉。
  • 源码与 dist 产物中 data-theme 命中均已归零;本条取代上文 2026-08-12 条目里「保留而未清理」的结论。

当日执行并通过:npm run build --workspace @openmathmodel/web(index 824.14 kB);node --test 178/178;六张样式表 PostCSS 重新解析全部通过。真实 Chromium(CDP)逐路由验收:首页、我的项目、赛题库、优秀论文、方法库、设置中心渲染与改前一致,论文年份筛选 / 设置下拉 / 项目排序三个液态玻璃弹层正常开合;页面计算样式核对 --ink #101720、--soft #f3f5f8、--line #e4e8ee、body #f3f5f8、.main #fff、.nav-item #465263、color-scheme: light,与改前取值相同。

2026-09-19 报障修复:实验阶段「连续 3 次被后端进程重启打断」即判死,且把 uvicorn 参数说明摊在页面上

用户带图报障:实验阶段连续三次「深度思考 · 实验执行(本次调用中断)」后运行落 FAILED,失败框里是整段给开发者看的话(interrupted: executor lost before completion、uvicorn --reload / --reload-dir / npm run dev:api / README)。三条拍板:这段不能显示到前端让用户看到;不要设置限额;以及一个疑问——「为什么总说后端进程重启打断,我明明没重启」。

排查:查库 run_8bc8eacd… / run_f0e1ff8c…(09-18)步骤表,实验阶段各趟都在 python_run 之后 2–12 秒被判 interrupted;Win32_Process 里当前 API 进程的命令行是 python -m uvicorn omm_api.asgi:app --reload --timeout-graceful-shutdown 5 --port 8000——没有 --reload-dir(也没有 --app-dir,靠可编辑安装从 VS Code 终端直接起,cwd 是仓库根或 backend/api,两者都盖住 backend/api/data/);已装 uvicorn 0.52.4 的 Config 在没给 --reload-dir 时监视 Path.cwd() 下全部 *.py,沙盒每次写出 backend/api/data/workspaces/<run>/steps/<step>/main.py 都触发一次热重载。所以「重启」不是用户做的,是 --reload 自动做的;用户看不到这层,文案却把责任说成「后端进程重启」,自然对不上。

  • 后端 engine_glue.advance_run:删除 MAX_CONSECUTIVE_INTERRUPTS / _interrupt_streak 与 fail_run 分支——被中断的阶段无上限自动重跑(attempt+1),运行不再因中断次数落 FAILED;INTERRUPTED_STEP_ERROR / StepStatus 两个只为它服务的导入随之移除(TaskRunEngine.fail_run 保留为引擎能力)。_note_executor_restart:给用户的 run.log{kind:"executor_restarted"} 改为「「X」的上一次执行在中途意外中断,已自动重新开始第 N 次尝试」——不带进程 / 热重载 / 启动参数任何开发细节(前端渲染路径不变);服务端 omm.engine warning 改为「left RUNNING by a lost executor」并附诊断:能确诊时直指监视目录与沙盒目录,否则给出通用的 npm run dev:api 提示。
  • 后端 新增 reload_guard.py:纯函数 reload_watch_dirs(argv, cwd)(与 uvicorn Config 同口径:--reload-dir / --reload-dir= / 环境变量 UVICORN_RELOAD_DIRS,缺省 cwd)与 reload_watch_hazard(argv, cwd, workspaces_dir)(沙盒工作区落在监视目录内即命中;目录形式的 --reload-exclude 盖住工作区算排除,glob 形式不算——uvicorn 过滤器只拿它匹配文件名末段);warn_if_reload_watches_sandbox 在 create_app 的 lifespan 里、推进线程启动前调用,命中就以 ERROR 打一块 !!! 包住的日志(uvicorn reload 子进程继承父进程的 sys.argv 与 cwd,所以能在子进程里看出来)。API 不拒绝启动——生产没有 --reload,这层只对开发者说话。
  • 测试 test_executor_restart_note.py:叙述断言改为新文案并加「不含开发词」守卫;原「三次打断后停下」用例替换为「连续 5 次打断运行始终 RUNNING、无 failure,环境修好后第 6 趟直接成功、叙述 5 条」。新增 test_reload_guard.py 8 项(非 reload 不报;裸 --reload 命中并给出目录与命令;--reload-dir 限定源码不报;显式 --reload-dir backend/api 仍命中;目录排除清除 / glob 排除不清除;环境变量等价;相对 workspaces_dir 按 cwd 解析;启动函数按真实 argv / cwd 打 ERROR 或不打)。
  • 文档:backend/api/README.md(启动命令下补一段「漏 --reload-dir 时会看到什么」,预算说明里的 MAX_CONSECUTIVE_INTERRUPTS 改为指向 reload_guard)、根 README.md(同段措辞)、.env.example 注释;engine_glue._env_limit 的 docstring 同步。
  • 不动:INTERRUPTED_STEP_ERROR 常量与 TRANSIENT 归类(前端「重试当前阶段」路径不变);工作区目录位置(backend/api/data/workspaces)——它是跨阶段共享的执行暂存,不为躲开监视范围搬家;tools/dev-api.mjs / npm run dev(本就带正确参数)。

当日执行并通过:pytest backend/api/tests 479 passed / 2 skipped(含新增 11 项)。实机复现:用同款参数(--reload、无 --reload-dir)从仓库根起一个只装了护栏的探针应用,uvicorn 日志 Will watch for changes in these directories: ['E:\\…\\OpenMathModel'],子进程 argv / cwd 与父进程一致,护栏按预期打出 ERROR 块。随后停掉那条没有 --reload-dir 的进程(Win32_Process 确认其 reloader 的 cwd 是 backend/api,监视范围盖住 data/workspaces),以 npm run dev:api 重启,/api/health ok、监视目录只剩 agents 与 backend/api/omm_api。浏览器验收待用户:在失败的任务上点「重试当前阶段」,实验阶段的 python_run 之后不再出现「本次调用中断」;即便出现,活动流只会补一句「上一次执行在中途意外中断,已自动重新开始第 N 次尝试」,运行不再因此落 FAILED。

2026-09-19 报障修复:「我的项目」卡片上「实验」「论文」两列永远是灰色「暂无统计」

用户带图报障:项目卡片的「文件」有数字,「实验」「论文」却对所有项目(包括已完成的)一律灰掉,什么都不显示。原因是切片②只在契约 Project.stats 里定义了 latest_run 与 artifact_count,projects-page.ts 的 rowHtml 把后两列硬编码成 —,展示层再把 — 译成灰色「暂无统计」——数据源从未接上,而不是数据为空。

  • 契约 project.schema.json:project_stats 新增可选字段 experiment_count / paper_count(非负整数)。口径 = 该项目全部运行中 EXPERIMENTING / PAPER_WRITING 节点 现行(stage_outputs.status=current)阶段输出条数:重做 / 重试只保留最新版、模拟链路不落行故计 0,与成果页只列最近一趟产物的口径一致。做成可选而非 required 是因为 check_compat.py 把新增 required 判为破坏性变更;旧生产者不返回时前端仍显示「暂无统计」。夹具 project.1/3.json 补字段,新增反例 project.bad-experiment-count.json;TS / Pydantic 重新生成,手工维护的 src/index.ts 同步;OpenAPI 基线重导。
  • 后端 routers/projects.py:stage_output_counts_subquery() 经 task_runs 把 stage_outputs 归到项目、按节点 sum(case) 一次算出两列,include=stats 时与既有产物计数子查询一并 outerjoin,仍是单条查询、无 N+1。
  • 前端 projects-page.ts:ProjectItem 增加 experimentCount / paperCount,三列计数统一走 count(),— 只在旧服务端缺字段时出现。projects-presentation.ts 与样式不动。
  • 测试 test_projects.py:新增「两次运行、首轮实验被重做、另有 VALIDATING / DATA_PREPARATION 行」用例断言 experiment_count=2 / paper_count=1、superseded 与其他阶段不计、q 过滤后计数不变;既有空统计断言改为含两个新字段。

当日执行并通过:validate.py(16 schema / 75 fixture)、check_compat.py OK、generate_python.py --check、npm run check --workspace @openmathmodel/contracts、export_openapi.py --check、pytest backend/api/tests 479 passed / 2 skipped、npm run check(web)。只读探针核对本地库:3 个已完成项目均为实验 1 / 论文 1,2 个失败项目为实验 1 / 论文 0。真实 Chromium 验收:注册一次性 smoke 账号,白盒造「首轮实验 superseded + v2 current、论文 v1 current、MODEL_PLANNING current」与一个从未运行的对照项目,网格卡片显示「文件 0 · 实验 1 · 论文 1」与「0 · 0 · 0」,列表视图三列同值且无 project-stat-unavailable 灰化;验收后账号与两个项目已删除。

2026-09-27 执行轨迹两级折叠、agent 旁白,以及真实产物被误标「(模拟)」

用户带图反馈任务执行页的执行轨迹:小标题太多,连续做的同类事情应归进可折叠的二级标题;「现在怎么都是写的模拟」;轨迹全是小标题、太单薄,要加 agent 的话。三条拍板:两级折叠;每步一句中文说明(改工具协议提示词);存量误标一起改。

排查:「模拟」来自 engine_glue._ARTIFACT_NAMES 按产物 kind 起展示名——这张表本是模拟链路(SimStageNode 的 baseline-metrics.svg / report-draft.md)用的,却套在所有产物上,真实实验画的每张图都登记成「基线实验结果图(模拟)」(本地库 6 个运行、34 张图,artifact.published 事件同名)。轨迹单薄:ws_read / ws_list / env_probe / subagent:* / 各类 *_review 没有专门分支,落进「标题 = 工具名、详情 = 原始 JSON」兜底,审稿人的结论和总结也被当 JSON 藏起来;既有 appendGrouped 只合并紧挨着的同 key 行,思考行与工具行交替出现,从未触发;沙盒 / 审稿 / 提议人走文本协议,模型在 JSON 前写的那句说明只进 llm_delta 实时框,落定即丢。

  • 后端 engine_glue._ARTIFACT_NAMES 改按 (kind, 文件名) 精确匹配,只认模拟链路与论文草稿自己的文件,其余产物按 URI 尾部的真实文件名展示(stage_outputs._artifact_file_name 的 docstring 同步)。llm.py 新增 lead_in_note:取文本协议回复里 JSON(或代码围栏)之前的那句话,超 300 字截断,混有模型原生工具调用标记(DeepSeek 偶尔把 DSML / 尖括号标签漏进正文)时丢弃;EngineLlmPort.chat_text 在调用落定后发 run.log{kind:"agent_note", prompt_id, text}。
  • 提示词 chat_adapter.tool_protocol_note:「只输出一个 JSON 对象」改为「先用一句简短的中文说明这一步要做什么、为什么(原样展示给用户,不要花括号),紧接着一个 JSON 对象」;适配器解析规则不变。
  • 迁移 0021_artifact_display_names:把 artifacts.name 与 artifact.published 载荷里误标的名字改回 URI 尾部的真实文件名,模拟链路自己的文件不动;纯数据修正、可重复执行,downgrade 不回写错误名字。
  • 前端 新增纯函数模块 integration/trace-rows.ts:工具行人话(「读取 experiment.py」「写入 …」「生成数据画像:…」「已在沙箱执行实验代码」等)、子代理组标题(单个「角色:任务」,并行「角色 ×N:各自视角」)与收束状态、审稿结论(加粗一句结论 + 审稿人总结原话)、论文补图 / 成稿 / 定向回改叙述、过程组标题;isQuietTool 标出不进轨迹的工具——列目录(ws_list,「查看工作区文件」「查看 data/ 下的文件」)、检索知识库、查阅知识卡片、探测运行环境(env_probe)这类摸底动作对用户没有信息量,成败都不画,事件照常落库(同日复看时用户点名去掉,组行上「思考 3 次 · 运行代码 2 次」这类计数摘要也一并去掉;读文件 ws_read 与数据画像 table_profile 经用户确认保留显示);原控制器里的 STAGE_BY_PROMPT 迁入并补齐方案提议 / 汇总 / 定稿、稳健性检验、各审稿与论文补图,多语言模板后缀同口径。modeling-workspace-controller.ts 两级折叠:subagent:<kind> 的 spawn / result 不再单独成行,而是开 / 收一个子代理会话组——并行派发共用一组、最后一个 result 收束,组行走秒并带失败后缀,阶段推进 / 失败 / 进程重启时收组,会话期间落点换了就在新落点接「(续)」段;会话之外连续的思考 / 工具 / 旁白行归成过程组(按提示词说在做什么),被叙述、阶段产出、审稿结论等关键节点隔断就另起一组;组行下方显示组里最新一句旁白。auto_redo_exhausted / redo_requested 并入叙述行分支。
  • CSS workflow-refresh.css 只增量追加:.stream-group-body[hidden] 补 display:none(折叠体的作者级 display:flex 压过 UA 的 [hidden],既有「写入产物文件 ×N」组此前从来收不起来)、组头旁白单行省略、组内旁白样式。
  • 测试 新增 test_llm_agent_note.py 6 项;新增 test_artifact_display_names_migration.py(在 0020 上造数据再升到 head,断言真实图改名、模拟文件不动、回退再升级结果不变;Config() 不带 alembic.ini——env.py 见到配置文件就 fileConfig,默认禁用已存在的 logger,本文件按名字排在最前,会让其后靠 caplog 断言的 test_db_ready / test_graph_mode 收不到日志);test_task_runs_llm_nodes.py 端到端断言真实链路产物名不含「(模拟)」;test_chat_adapter.py 加 2 例;trace-rows.test.mjs 10 项。

当日执行并通过:pytest backend/api/tests 486 passed / 2 skipped;agents 各包测试 761 passed / 1 skipped;npm run check;npm run build(index 843.69 kB);node --test 188/188。开发库 alembic upgrade 到 0021,只读核对真实产物误标 0,剩余 29 个运行的「(模拟)」产物与事件都是模拟链路自己的文件。无头 Chrome(CDP 拦截 /api 回放只读夹具)截改前改后图(audit-current/2026-09-27-trace-grouping/,不入库):截图所用运行的执行轨迹顶层 254 项 → 72 项(24 个组),原始工具名行 85 → 0,「(模拟)」14 → 0;SSE 中途接入与整段回放结构一致。复看后(quiet-* 截图):夹具里 28 条列目录 / 检索知识库调用不再画出,全部行 244 → 213,组 24 → 21(去掉列目录行后只剩一行的不再套组),计数摘要 0;再藏探测运行环境后(quiet-probe-* 截图):10 条 env_probe 不再画出,全部行 213 → 203,组 21 → 20。未验:新提示词下真实模型写出的中文旁白——存量运行没有 agent_note 事件,截图里的旁白演示派生自改提示词之前的英文输出。

2026-10-02 接待改为「轮次理解 → 路由守卫」,拿不准先提议;运行页只把补充要求记为备注(ADR-0024)

用户反馈「意图识别 + 状态判断 + 路由守卫整体有问题」,列出意图、是否要求执行、是否需要建模、事件、会话状态、续接还是新任务、言语行为、题型、缺失信息、把握、路由十一个维度。复现(判定打桩、不出网):首页「美赛和国赛有什么区别」「我想学数模,从哪开始」「这张图是什么意思」+ 截图、「看看」+ PDF、长篇提问都直接建任务;首页对话里「好的,开始吧」孤立判定、放行后 goal 只剩这一句;未配置接口时「你好」也建项目;接待结论被前端丢弃,对话模型不知道路由;首页对话不带附件。运行页「谢谢」「辛苦了」「太慢了吧」「怎么还没好」「这个模型的精度多少」、贴来的新题全部记成补充要求注入后续节点。用户拍板三处策略:拿不准先提议、确认再启动;首页确认 = 对话里回一句;未配置接口也用本地规则拦。

  • 接待(intake.py 重写):本地信号(赛题标识 / 任务信号 / 问句 / 知识问句 / 请求与执行措辞 / 寒暄 / 光秃秃的「开始」/ 指代上文 / 修改 / 附件指代 / 题型)先判,拿不准的交最弱模型做一次 JSON 判定,判定只给 intent(五值)/ execution / continuation / domain / missing / confidence,不写回复;route(start / propose / clarify / reply)由规则定,顺序与取值见 ADR-0024 §1。附件与 @ 引用只是证据;问句判断按长度分档(60 字内任何问句标记都算,更长只认句首句尾);needs_info 的本地否决收窄为「要求动手 → start,没说 → propose」;判定失败只有执行措辞 + 任务信号 / 题面 / 附件才 start。
  • 契约:TaskIntakeInput 加 history / pending_task / confirmed;TaskIntakeResult 加 route / understanding / task_goal / context_excerpt / chat_ready,intent 旧三值保留并与 route 对齐;ChatRequest.intake(首页对话轮的接待结论)。OpenAPI 基线已重导出。
  • 首页对话(routers/chat.py):带 intake 的轮换用首页系统提示词(不得自称已开始建模)并注入【接待判定】块(提议时复述题意并问是否开始、缺信息时说清缺什么、问知识时直接答、问文件时按附件内容答);托管轮首个 meta 事件带 intake 落 meta.intake;任务页的轮忽略它。engine_glue._attachments_summary 把 params.conversation_context 排在最前、单独封顶 1500 字。
  • 运行页(run_control.py):判定 JSON 加 utterance(supplement / question / feedback / chat / new_task),只有 supplement 落备注;判定缺席时本地兜底(classify_utterance:新题 / 情绪短句 / 问句 / 寒暄不进备注,其余照旧记下);超过 800 字仍不送动作判定,但做一次只分类的判定(judge_utterance,带本任务题面);new_task 的状态块让模型提示回首页新建。问句标记补「怎么 / 多少 / 什么 / 哪个 / 哪些 / 哪里 / 多久 / 是否 / 有没有」。执行安全(合法集、提案确认、回复后执行)不变。
  • 前端:home-chat.ts 记着首页对话原话与上一轮提议(homeIntakeContext / rememberIntake),重进时从最后一轮的 meta.intake 恢复提议;对话轮带 intake 上送;托盘里还没注入过的附件正文随这一轮送给模型(collectConversationAttachments 新增 only 过滤,每个附件只注入一次,托盘保留作任务材料);chat_ready=false 时直接展示模板回应、不发起对话轮。task-start-controller.ts 随接待判定上送 history / pending_task(确认页 confirmed),start 时 goal 用 task_goal、params.conversation_context 用 context_excerpt,两者写进草稿(TaskDraft.task_goal / conversation_context)供失败重试。受保护入口、页面模板与路由零改动。
  • 测试:test_task_intake.py 重写为 23 例;test_run_control.py +6(本地兜底表、判定类别解析、新题状态块、三条端到端);test_chat_turns.py +5(【接待判定】注入与 meta.intake 落库、无 intake / 任务页不变、无状态通道、校验、问题分析摘要);task-start-state.test.mjs +1。

当日执行并通过:pytest backend/api/tests 532 passed / 2 skipped、npm run check --workspace @openmathmodel/web、npm run build --workspace @openmathmodel/web(index 848.58 kB)、node --test(194/194)、npm run check --workspace @openmathmodel/contracts、export_openapi.py --check(基线已重导出)、check_compat.py、validate.py。未验:真实模型下的判定质量(用例全部打桩),以及首页「提议 → 回一句开始 → 进入执行页」的浏览器走查。

2026-10-03 题目信息不足带错误码 E610;任务页对话里补题面后自动重新解析

真实运行 run_278ee396(题面被截断的能源调度题)18 秒失败于题意解析:节点判「题目信息不足」却不带错误码,_classify_failure 按文案没命中、兜底成 CODE_DEFECT,E6 看板记「无错误码」;失败文案让用户新建任务,下面却跟着「重试当前阶段」——同输入重试只会再判一次不足(08-28 那条早就点过「语义上是输入问题,却被渲染成带重试按钮的工作流故障」)。用户 1 分钟后新建任务重贴了完整题面。拍板两处:方向 = 任务页对话里就地补题面、补完自动重新解析;错误码 = 新开 E6xx 输入码段,只开 E610。

  • 错误码(agents/core/src/omm_agent_core/errors.py):ErrorCode.INPUT_INSUFFICIENT = "E610" + Disposition.NEEDS_INPUT(用户补了输入才重试)+ CATALOG 一条(owner node);E6 看板 by_code 经 CATALOG 带出 owner / 处置。
  • 题意解析(agents/skills/src/omm_agent_skills/nodes.py):判 insufficient → NodeResult.failed(..., error_code=E610);文案改为「可以直接在本任务的对话里补充缺的内容(整段粘贴题面也行),补充后会自动重新解析题意;也可以新建任务、附上完整题面与数据文件重新发起」。
  • 失败归类(engine_glue.py):_classify_failure(error, error_code=None) 码优先(E610 → DATA_DEFECT,契约 FailureClass 八类不改),再走文案规则、最后兜底 CODE_DEFECT;STEP_FAILED 投影传载荷里的码。RUN_FAILED 载荷不带码 → last_step_error_code:按 seq 倒序取最近一条步骤事件(STEP_STARTED / STEP_SUCCEEDED / STEP_FAILED),是 STEP_FAILED 才取它的码——之后又开过步骤,更早那次失败的码就不代表这次失败。
  • 运行页(run_control.py):10-02「运行页只把补充要求记为备注」在这里有一个例外。RunControlContext.awaiting_problem_supplement(FAILED、停在题意解析、码 E610 三者同时成立)时,失败运行上不触发动作的话照样分类(超 800 字的长文走只分类的 judge_utterance),判为 supplement 就改判 retry:原话加「【题面补充】」前缀落成 scope=PROBLEM_ANALYSIS 的备注(只进题意解析的提示词,上限放宽到 2 万字),随后自动重试,回执说「已把你补充的题面交给『题意解析』,重新解析题意」;判定提示词在这个状态下说明补的题目内容(包括整段粘贴同一道题)算 supplement、明显是另一道题才算 new_task。问句 / 新题照旧只回对话;别的原因失败(含题意解析的其它码)不受影响。运行输入 goal 记在 RUN_CREATED 里、事件溯源不可改,且只有题意解析读原始题面,所以补充走备注而不是改题面。
  • 前端:零改动——失败文案本身就是引导。已知局限:「重试当前阶段」按钮照旧跟在失败叙述下面,不补题面直接点 = 同输入再判一次不足(多花一次模型调用);隐藏或改写它属于界面交互改动,本次不做。事件日志不可变,历史失败不回填码。
  • 测试:test_failure_error_codes.py +2(码优先于文案;判不足的运行步骤与运行分型都是 DATA_DEFECT、看板 uncoded == 0);test_run_control.py +6(状态判定只在三条件同时成立时为真;计划与判定提示词;端到端:真题意解析节点 + 打桩模型,判不足 → 对话补题面 → 备注 scope 与前缀 → 自动重试 → 第二次解析的提示词同时带原题面与补充 → 通过,last_step_error_code 先 E610、通过后为空;长文补充;问句不动作;普通失败运行补要求照旧不重试);core test_errors.py、skills test_nodes.py 在既有用例里加断言。

当日执行并通过:pytest backend/api/tests 552 passed / 2 skipped;agents 五包 + worker 844 passed / 1 skipped;strict mypy 与 ruff 对照改动前零新增;临时 worktree 检出改动前代码跑新测试,5 条行为用例全红、守卫用例全绿。未验:真实模型下的补题面判定与重新解析(用例全部打桩)。注意 npm run dev 拉起的 API 不带热重载、且会复用 8000 端口上已在运行的 API——要在真实运行里用上这次改动,须把旧 API 进程整个停掉再启动。

2026-10-03 模型原生工具调用标记兼容;每一步记下执行代码的身份,旧 API 进程告警

复盘 10-01 以来的三个真跑时查出两件事。①run_e898… 里提议人两次 knowledge_search 收到空参数,看上去像 10-02 的扁平信封容错没生效;实际是它跑在 10-02 那次修复之前起的 API 进程上——npm run dev 拉起的 API 不带热重载,还会复用 8000 端口上已在运行的 API,改了代码不整个重启,真跑就一直用旧代码,而事件日志里没有任何字段能说明一步是哪版代码跑的,只能靠旧代码的行为特征倒推。②09-01 以来 322 条文本协议回复里有 10 条夹带 DeepSeek 原生工具调用标记(<||DSML|| invoke name=…> 加 parameter 块):其中 4 条只有原生标记、没有 JSON 信封,适配器把它当终答、解析失败,结构修复提示把本想调工具的模型推去交卷,白烧一轮;另外 6 条「JSON 信封 + 原生标记」里,原生那个调用被静默丢掉,模型不知道它没执行。

  • 原生调用兼容(agents/skills/src/omm_agent_skills/chat_adapter.py):正文里的 DSML invoke 块按出现顺序还原成信封(竖线全角 / 半角、个数与空白放宽;string="false" 的参数值按 JSON 解析、解不开保留原文;只有一个名为 arguments 的参数时它就是整个参数对象;name 为空的块跳过)。没有 JSON 信封时执行第一个原生调用,下一条工具结果末尾提示「已当作信封执行了 X、后面的 Y 没有执行,之后请直接写 JSON 信封」;与 JSON 信封同时出现时只执行信封,并点名原生标记里没执行的调用(与已执行信封同工具同参数的不算)。半截标签、终答里提到 DSML 照旧当终答。
  • 执行代码身份(新增 backend/api/omm_api/code_identity.py):API 进程启动时给它执行的源码拍快照——omm_api / omm_contracts / omm_agent_core / omm_agent_harness / omm_agent_skills / omm_agent_tools 六个包的 *.py 与 agents/prompts/*.prompt.md(提示词注册表同样是进程内缓存):逐文件 sha256 汇总成 12 位指纹;git HEAD 与分支直接读 .git(loose ref → packed-refs → linked worktree 的 commondir);相对 HEAD 有改动的源码文件在拍照时跑一次 git --no-optional-locks status(不刷新索引、不抢 index.lock),git 不在或报错记「未知」而不是「没改动」。快照约 130 ms、只拍一次。
  • 步骤盖章(engine_glue._ProjectingSink):STEP_STARTED 领域事件落库时 payload 多一个 executor:code(启动时指纹)、git(head / branch / dirty_count / dirty 最多 8 个)、pid、started_at;进程启动后磁盘源码又改过时加 stale(count / files 最多 8 个)。只进 run_domain_events:v1 step.started 投影与页面不变;引擎内存里的事件不带它,重放时 reducer 忽略多出的键;盖章出错只缺章、不拦步骤。
  • 旧进程告警:「改过」先比修改时间与大小,对不上再比内容摘要(切分支再切回、原样保存不算改动),每 5 秒最多查一次。步骤照常执行,盖章记下 stale,服务端日志打一块醒目 WARNING(同一组改动只打一次,改动多了再打)说明本进程不热重载、这一步按启动时的旧代码执行、怎么重启。GET /api/system 增加 code:available / code / git / dirty(改动文件数)/ started_at / stale / stale_files——只给计数,不带文件名与路径。tools/dev-local.mjs 复用已运行的 API 时打印它的代码版本与启动时间,旧进程就醒目提醒先停掉再启动;自己拉起的 API 在健康检查通过时也打印代码版本。
  • E6 看板(agents/harness/src/omm_agent_harness/metrics.py):报告新增 code 节——按指纹归并的版本列表(git / 相对 HEAD 改动数 / 步骤数 / 旧进程步骤数 / 阶段,按首次出现排序)、stale_steps、processes、unstamped_steps(本次之前的运行与评测会话没有章,如实计数、不推断版本);Markdown 加「代码版本」一节;批次 totals 加 stale_steps / unstamped_steps。端点响应模型 RunMetricsReport 的顶层键是固定字段,新键不加进去会被静默丢掉,因此补了 code,OpenAPI 基线重新导出。
  • 测试:test_chat_adapter.py +4(真库原文还原、原生调用回提示、信封与原生标记并存、半截标签 / 空 name 照旧当终答);新增 test_code_identity.py 15 项(快照口径、git 身份四种形态与真仓库的改 / 删 / 重命名 / 未跟踪、节流与只告警一次、快照失败不盖章、领域事件盖章而 v1 投影不变、旧进程盖章、/api/system 不带路径、E6 端点读回);test_e6_metrics.py +2;test_run_metrics.py / test_metrics_home.py 顶层键加 code。backend/worker 的 test_lease.py 两个依赖墙钟余量的用例(被抢的租约与抢到的租约共用 50 ms TTL)改为同目录开两个存储——该过期的一方短 TTL、抢 / 续的一方 60 s,负载下失败率 1/100 → 0/100,lease.py 未改。

当日执行并通过:pytest backend/api/tests 567 passed / 2 skipped;agents 五包 + worker 850 passed / 1 skipped;strict mypy 与 v3.64 基线逐条相同、ruff 对照改动前零新增;export_openapi.py --check;临时 worktree 检出改动前代码跑新测试,行为用例全红(原生调用 3 条、代码身份 5 条)、守卫用例全绿。未验:真实模型下原生调用还原后的工具执行(用例用真库原文打桩);旧进程告警的实机走查。要在真实运行里用上 10-03 的几次改动,仍须把旧 API 进程整个停掉再启动——之后的运行可以在 E6 看板「代码版本」一节直接核对每一步跑的是哪版代码。

2026-10-03 审批卡「上一轮反馈」:回退重做轮的闸门写明这轮为什么重做、上一轮错在哪

回退重做有三种来由:用户在对话里要求从某阶段重做、在闸门上选了重做选项、图按条件边自动回退。reducer 在回退落地前把被作废的上一轮产出打成反馈包 TaskRunSnapshot.iteration_feedback(回退原因、谁触发的、第几轮、作废了哪几段),重做的节点与三条审稿环一直在读,拍板的人却看不到:重做轮的 G1 / G2 / G4 审批卡与首轮一模一样,只有 G3 在标题里多一句「较上一轮」。契约里工作台视图 pending_approval.description 与审批接口 ApprovalRequest.description 两个字段早就定义了,却一直是 null,前端也从未渲染。用户拍板:卡片里加「上一轮反馈」块——后端把摘要写进 description,前端在选项上方渲染,并重启开发服务做浏览器验收。

  • 摘要口径(新增 backend/api/omm_api/approval_feedback.py):feedback_digest(feedback, gate_state) 把反馈包摘成几行。第一行是来由:从哪一段起回退重做、谁触发的(按你的要求 / 在某阶段的闸门上选了重做 / 图按条件边自动回退)、第几轮;之后每行一条「- 」开头的事实:你的要求或回退原因(自动回退去掉「图 X 条件边自动回退:」前缀,闸门选项 id 不另列)、作废的上一轮产出,再按闸门所在阶段取材——结果验证(G3)取上一轮未过的稳健性检查(检验脚本真跑通才算,口径同 G3 闸门)与实验审稿、检验脚本审稿未解决的阻断意见,论文撰写(G4)取上一轮终稿审计发现(分类计数 + 前 3 处)与质量告警,建模方案(G1)取上一轮推荐的方案与候选数,数据准备(G2)取上一轮的画像与清洗结论。每类只点名前几条,引用原文与回退原因单独封顶。只转述事实、数字原样,不评判本轮好坏;首轮闸门(没有反馈包)和位于回退起点上游的闸门不出。
  • 接线(engine_glue.py / workspace_view.py / serialize.py):_ProjectingSink 绑定引擎正在推进的快照(open_engine 重放后 sink.follow(snapshot)),REVIEW_REQUESTED 投影成审批行时把摘要存进 evidence["feedback"]——开门那一刻算好,之后读视图不再重放事件。工作台视图与审批接口的 description 都由它生成,按契约上限(2000 / 4000 字)在整行处截断。契约、OpenAPI 基线、引擎与 reducer 不动。
  • 前端:新增纯函数模块 integration/approval-feedback.ts(describeApprovalFeedback:description → 来由 + 逐条事实 + 幂等戳,不碰 DOM);modeling-workspace-controller.ts 新增 renderApprovalFeedback,在 followConversationTail 之后调用,把块贴在每个主操作按钮上方「选项列表 / G4 证据块」整组之前(按属性认块,不打乱它们按「前一个兄弟」认位置的约定),门解决或首轮闸门即摘掉;workflow-refresh.css 只增量追加 .approval-feedback*(与 G4 证据块同一套紧凑排版、左侧色条,紧跟主操作时补 12px 上边距);英文词典加「上一轮反馈」。受保护入口、页面模板与路由零改动。
  • 测试:新增 test_approval_feedback.py 8 项(无包与上游闸门不出;G3 自动回退逐行全文、检验脚本没跑通不列检查;G4 闸门选项回退的审计分类与质量告警、零发现;G1 用户原话 + 推荐方案,同一个反馈包下游的 G3 只给来由与作废范围;G2 画像与清洗;坏数据不抛;长文整行截断;端到端:模拟链首轮 G1 无 description → redo_run → 重做轮 G1 视图与审批接口同文、过两份契约 schema、审批行存了摘要、被作废的首轮门没有、批准跑完卡片消失);新增 approval-feedback.test.mjs 4 项。

当日执行并通过:pytest backend/api/tests 575 passed / 2 skipped;npm run check --workspace @openmathmodel/web;node --test 236/236;npm run build --workspace @openmathmodel/web;strict mypy 与 v3.65 基线逐条相同、ruff 对照改动前零新增;临时 worktree 检出改动前代码、只放入新模块跑新测试,接线用例红、7 条纯函数用例绿。浏览器验收(开发服务 + 模拟链测试运行,截图在 audit-current/h4-approval-feedback-20261003/,不入库):首轮 G1 没有块 → 从建模方案重做(附言「换成更简单的贪心调度方案,先保证能跑通」)→ 重做轮 G1 在运行页与建模方案页的主操作上方出现块(来由 + 你的要求 + 作废的上一轮产出)→ 点「确认 Agent 当前方案并继续」后块随门摘掉,运行跑完后终态页也没有残留。未验:G2 / G3 / G4 重做轮的块在真实运行里的样子(模拟链批准 G1 后直接跑完、不再开门,这三道门的摘要口径只由单测覆盖);模拟链的建模方案没有候选方案,验收里看不到「上一轮推荐的方案」一行;测试账号没配模型接口,运行页对话走不到「确认重做」那步,验收里的重做直接调用对话控制面同一个 engine_glue.redo_run。

2026-10-03 worker 执行面也记下执行代码的身份;代码身份实现移入 harness

同日那次只在 API 进程里给步骤盖代码身份:backend/worker 执行面(H6 原型,事件写进 runs/agent-runtime/events/<run_id>/events.jsonl,同一份日志也是重放来源)推进的步骤没有章,E6 看板只能记成未盖章;而代码身份模块放在 omm_api 里,worker 不依赖 omm_api,用不上。用户拍板:通用实现搬进 API 与 worker 都依赖的 harness,API 留适配层;评测会话不盖,只盖真实 worker。

  • 通用实现(新增 agents/harness/src/omm_agent_harness/code_identity.py):快照、指纹、git 身份、旧代码检查、盖章与同一组改动只告警一次的逻辑原样搬入;快照哪些包(packages)、提示词模板目录(prompts_dir)、告警里怎么称呼进程(process_label)与怎么重启(restart_hint)改成 ProcessCode 的参数。harness 不导入 skills,提示词目录由各执行面传入。
  • API(backend/api/omm_api/code_identity.py):收成适配层——API 的包清单(omm_api / omm_contracts / omm_agent_core / harness / skills / tools)、「API 进程 / npm run dev 先 Ctrl+C」的告警口径与进程级单例。engine_glue 盖章、main.py 启动时拍照与 /api/system 摘要的调用方式不变,告警原文不变。
  • worker(新增 backend/worker/src/omm_worker/code_identity.py):worker 的包清单(omm_worker / core / harness / skills / tools,执行面不导入控制面)与「worker 进程」告警口径;StampingSink 包住 JsonlEventStore,STEP_STARTED 落盘前复制一份加 executor(code / git / pid / started_at,启动后源码又改过时加 stale),引擎内存里的事件不动,盖章出错只缺章、照常落盘。WorkerRuntime 构造时先拍快照(记推进任何步骤之前的代码),新参数 stamp_executor 缺省开;评测的 build_runtime 用固定时钟与顺序 id、事件日志要逐字节可复现,传 False 关掉。reducer 处理 STEP_STARTED 只读 state / step_id / attempt,带章的日志重放结果不变。
  • E6 看板:口径不变;worker 的事件交给 aggregate_run 即可读出代码版本,评测会话照旧记未盖章。
  • 测试:harness 新增 test_code_identity.py 12 项(原 backend/api 的 10 条通用用例随实现搬来,另补按包名解析源码根、不给 roots 时按包快照 2 条);backend/api 的代码身份用例收为 5 条接线用例(旧进程用例改用适配层并断言 API 口径的告警);worker 新增 6 项(日志里步骤开始事件带章、其余事件不带、E6 读回;旧进程盖 stale 且按 worker 口径只告警一次;盖章出错不拦步骤;关掉开关不盖;带章日志重放与实时快照一致;真进程快照只覆盖执行面的包);评测 +1(评测会话不带章)。

当日执行并通过:pytest backend/api/tests 565 passed / 2 skipped(10 条通用用例移到 harness);agents 五包 + worker 869 passed / 1 skipped;strict mypy 与改动前基线逐条相同(多检查两个新模块)、ruff 对照改动前零新增;临时 worktree 检出改动前代码、只放入两个新模块跑 worker 新测试,3 条接线用例红、3 条守卫用例绿;再放入运行时接线但不关评测,评测「不带章」用例红、金轨迹照绿,补上评测开关后全绿。未验:长驻 worker 进程的实机走查(worker 仍是原型,没有生产调用方)。

2026-10-05 沙盒收尾运行通过后,删掉失败运行留下的文件

10-03 起沙盒有「收尾运行」:波末最后一次运行没过验收、而更早某次运行的结果能全过时,执行体把那次的代码原样复跑一遍收尾(26b113d),同名文件被覆盖回那一版。但后来失败的运行新写出、那一版没写过的文件仍留在工作区——审稿卡的「工作区文件」清单、实验阶段列的数据文件、论文补图可读的数据、之后各沙盒里模型自己 ws_list 看到的都是工作区原样;节点又按「类型 + 文件名」累加每次运行报上来的产物、只增不删,失败运行独有的图和结果表也进了阶段产出。收尾挑来源时用的是当前工作区清单,「至少有一张图」这类断言还可能靠失败版的图过关。用户拍板:这些文件删掉,而不是只打标或完整还原工作区。

  • 执行器(agents/tools):SubprocessRunner.run 的结果新增 created_files / modified_files / deleted_files——本次运行新建 / 改写 / 删掉的工作区相对路径(排序,空的不列;超时的运行也报,它写下的文件同样留在工作区);产物采集改用同一份运行后快照,跑完只扫一遍工作区。ws_write 的结果加 created(写之前文件不存在)。TaskWorkspace.discard(paths) 只删工作区根内的普通文件,越界或绝对路径、目录、不存在的文件一律跳过、不抛,返回真删掉的。
  • 执行体(agents/harness/src/omm_agent_harness/sandbox_agent.py):按执行顺序记下每次模型运行、收尾复跑与 ws_write 新建 / 写过 / 删掉的路径和报上来的产物。遗留文件 = 晚于来源运行的执行新建、收尾复跑没有重新写出的路径;来源运行(含)之前写过的不算(那时它就在工作区里,包括模型用 ws_write 写的配置),来源运行之后被删过的也不算(可能是原本就在、被失败运行删掉又重建的文件,宁可留着)。run_sandbox_task(discard_files=…) 给了就开清理:挑来源时按那次运行自己写过的路径预估遗留,复跑后的重评按真实遗留,两处都把遗留当作不存在(断言看到的文件清单与读文件都没有它们)——通过不能靠失败版留下的文件,没有能诚实过关的运行就不收尾。复跑通过才以「路径 → 它们报过的产物 id」交给调用方删,删掉的文件报过的产物不再当指标来源(报告的 produced_artifacts 仍照列全部,作审计清单);复跑没过就进下一波,模型还要接着改,文件不动。没给 discard_files 时行为与之前一致。
  • 节点与装配:清洗 / 实验 / 检验 / 论文补图四个沙盒调用点经 _finalize_cleanup 接上。删除能力由执行侧经 services.extras["discard_workspace_files"] 注入——API 的 open_engine 与 worker 的 _open_run 都给本运行工作区的 TaskWorkspace.discard,拿不到就不开清理。删掉的文件报过的产物从本阶段采集里拿掉(不进阶段产出与图件清单,文件内容仍在产物存储里可查),执行轨迹补一句 agent_note:「收尾时删掉了失败运行留下的 N 个文件(复跑的那一版没有写它们):…」(最多点名 8 个)。契约、OpenAPI 基线与前端不动。
  • 测试:harness test_sandbox_agent.py +5(来源之后的调试运行与 ws_write 新建的文件被删,来源之前写过的、复跑重写的、只被改写的与下发的数据都不动,删掉的 metrics.json 不再当指标来源;来源运行没画图、失败的调试运行画了——不清理时会靠那张图判收尾通过,开了清理就不收尾、什么都不删;原本就在的文件被删掉又重建时留着;复跑没过不删;一次没过的收尾复跑之后再收尾,它新建的文件也算遗留);skills test_nodes.py +3(实验节点调试运行画的图被删、不进阶段产出、轨迹补旁白,复跑重写的结果表照留;执行侧没给删除能力就不开清理;采集拿掉产物后同名新版仍顶替原位);tools +4(运行报出新建 / 改写 / 删掉的路径、没变化不带这三个键;超时的运行也报;ws_write 报是否新建;discard 只删工作区内的普通文件);worker test_runtime.py +1、backend/api 新增 test_finalize_cleanup_wiring.py 1 项(删除能力经 extras 注入节点,只删本运行工作区里的文件)。

当日执行并通过:pytest backend/api/tests 566 passed / 2 skipped;agents 五包 + worker 882 passed / 1 skipped;strict mypy 与改动前基线逐条相同、ruff 对照改动前零新增;临时 worktree 检出改动前代码跑新测试,14 条新用例全红、同文件其余 264 条全绿,放入本次改动后 278 条全绿。未验:真实运行里的清理(需要一次「后面的运行失败、收尾复跑通过」的真跑;要用上这次改动,仍须把旧 API 进程整个停掉再启动)。已知局限:模型终答叙事(approach_summary / figure_notes)可能描述的是后来没过的版本,本次未处理。

2026-10-05 沙盒收尾通过后按采用的那次运行重写终答叙事;E610 失败的主操作改为「去对话里补充题面」

上一节留下的已知局限:收尾复跑通过时,模型的终答还是波末那份,写在没过验收的运行之后——实验的 approach_summary / progress_note、清洗 / 检验 / 论文补图的 summary 与 figure_notes 可能描述的是没被采用的版本,而阶段产出、实验审稿材料与执行轨迹正文都照抄它。最常见的收尾恰好是来源与失败的几次运行在同一波(第 1 次运行就出了指标、之后几次调参没过),「取来源那一波的终答」对它无效。10-03 E610 那一节也留了一条:题意解析因信息不足失败时,失败文案让用户去对话里补题面,下面的主操作却还是「重试当前阶段」,不补题面直接点只会同输入再判一次不足。用户拍板两处:收尾通过后让模型按采用的那一版重写叙事;E610 的主按钮换成「去对话里补充题面」(不是隐藏)。

  • 叙事重写(agents/harness/src/omm_agent_harness/sandbox_agent.py):run_sandbox_task(on_narrative_rewrite=…)。收尾复跑通过且没取消时,另开一个不带工具的内环(LoopBudget(max_turns=2):一轮交终答,多一轮兜住误调工具被退回;结构修复照旧一次),按同一套终答键与校验让模型重写。prompt 只给采用的那次运行的代码(保头 6000 字)、复跑 stdout(保尾 2000 字)、复跑的验收结果、工作区现有文件(最多列 80 个,图件说明只能写现有的图)与旧终答,不带任务说明(工具协议、审稿意见)与运行预算。写成就替换终答,on_final_answer 回传重写后那份;结构不合格、轮数用尽或调用出错都沿用原终答,任务照样通过;这次调用的用量计入报告。没触发收尾、收尾没过、已取消都不重写。
  • 节点(agents/skills/src/omm_agent_skills/nodes.py):_narrative_note 给清洗 / 实验 / 检验 / 论文补图四个沙盒调用点补一句执行轨迹旁白——写成了:「终答叙事已按收尾采用的第 N 次运行重写(原来那份写在没过验收的运行之后)」;没写成:带上原因(最多 120 字),说明沿用原终答、叙事可能描述的是后来没过验收的版本。下游(阶段产出、实验审稿材料、轨迹正文、图件清单)照旧读终答,拿到的就是重写后那份。
  • E610 主操作(backend/api/omm_api/workspace_view.py):_awaiting_problem_supplement(运行 FAILED、停在题意解析、最近一次步骤失败码是 E610,与对话控制面 run_control.awaiting_problem_supplement 同一口径)成立时,工作台主操作投影为 {"kind": "supplement", "label": "去对话里补充题面"};其余失败照旧 retry「重试当前阶段」。
  • 契约:modeling-workspace-view.schema.json 的 agent_action_kind 加 supplement,agent_action 联合加 supplement_agent_action(必须有目标路由,审批与 option 字段为空),两处都是 ADDITIVE;Python / TS 生成类型与 OpenAPI 基线用脚本重生成;fixture 新增 valid modeling-workspace-view.2.json(E610 失败运行的视图)与 invalid modeling-workspace-view.supplement-without-route.json。
  • 前端(integration/modeling-workspace-controller.ts):点 supplement 走 focusConversationInput——把同页 .chat-composer textarea 滚入视野并聚焦,不调用动作接口;发出去的补充由对话控制面按 10-03 的口径改判为带题面补充的重试。运行页与五个阶段页的对话输入框都在工作台根节点内、与主按钮同侧。受保护入口、页面模板与路由零改动。
  • 测试:harness test_sandbox_agent.py +7(重写生效;prompt 截断与必填键;重写不合格沿用原终答;重写时的工具调用被拒、不执行;重写抛错沿用原终答且任务仍通过;收尾没通过不重写;已取消不重写),既有 6 条收尾用例补一轮重写回复并断言模型调用次数;skills test_nodes.py 两条实验收尾用例补叙事断言(阶段产出用重写后那份、重写 prompt 带采用的代码而不带调试片段、旁白多一句);backend/api test_failure_error_codes.py 的 E610 用例补工作台视图(过契约 schema、主操作逐字段相等),非 E610 的失败仍是 retry;test_run_control.py 补「补题面前主操作是 supplement、补完不再是」。

当日执行并通过:临时 worktree 里改动涉及的 5 个测试文件 309 passed;validate.py(16 schema / 77 fixture)、check_compat.py;另起 worktree 检出改动前代码跑新测试,17 红 / 292 绿(harness 13 = 新 7 条 + 既有收尾 6 条多出的那轮重写,skills 2,backend/api 2),放入本次改动后 309 条全绿。与下一节的并行改动一起合回主工作区后跑全量:pytest backend/api/tests 572 passed / 5 skipped(PostgreSQL 576 passed / 1 skipped),agents 五包 + worker 889 passed / 1 skipped,node --test 237/237,npm run check / npm run build(web),契约 Python / TS 生成 --check / --verify 与 export_openapi.py --check;strict mypy 较改动前只多 1 条(生成的 SupplementAgentAction.label 用 constr(...),与生成器既有的同款错误相同)。浏览器验收(10-07,开发服务 + 测试账号的 E610 种子运行,模型接口是本机打桩,截图在 audit-current/2026-10-05-e610-supplement/,不入库):运行页主按钮由「重试当前阶段」变为「去对话里补充题面」;点击后焦点落在对话输入框且在可视区内,点击期间 0 个网络请求、运行状态与 updated_at 不变。未验:真实模型下的叙事重写质量(用例全部打桩),需要一次「后面的运行失败、收尾复跑通过」的真跑;要在真实运行里用上,仍须把旧 API 进程整个停掉再启动。

2026-10-05 多个建模任务真正并行:每个运行一个工作线程;「最大并发任务」默认不限

用户问「现在是不是不能同时跑多个不同的建模任务……我不要限制」。查明是两层:①建任务闸门——同一用户「排队中 + 执行中」的运行数达到上限就 409 CONCURRENCY_LIMIT,上限取设置中心「高级设置 · 最大并发任务」(下拉默认显示「3 个」,NULL 时用部署默认 3);②执行面单线程——RunnerThread 每个节拍取全部可推进运行、逐个串行 advance,真实节点一步就是一个阶段(实验动辄十几分钟),其它运行全部干等。开发库实证:当天两个真跑的 13 个步骤跨运行重叠 0 次,后建的那个三个阶段实际只干了约 2.5 分钟、却等了 44 分钟,每个阶段都恰好在另一个运行的步骤结束那一秒才开始。设置面板的说明文字自己就写着「调大不等于并行加速」,system-overview §9 也列为已知缺口。用户拍板:执行面改成真并行;「最大并发任务」默认不限、保留可选上限;存量数据只清掉随旧面板默认值存下的 3。

  • 执行面(backend/api/omm_api/runner.py):RunnerThread 改成调度线程 + 每运行一个工作线程——每个节拍给每个可推进、且没有步骤在途的运行派一个 omm-runner-<run_id> 守护线程推进一步(仍是一步一派,模拟节点的节奏不变),运行之间互不等待,并行数不设上限。在途表保证同一运行同一时刻只有一个工作线程:advance_run 开头的 heal_interrupted 会把同一运行在途的 RUNNING 步骤判成 executor lost 重跑,正依赖这一点。单个运行推进抛错只记日志、让出在途位,下个节拍照常再派,不再像串行时那样中断整轮、连累排在后面的运行。新增 in_flight() / wait_idle(timeout);线程名 omm-mock-runner → omm-runner;WorkflowAdvancer.advance 与 engine_glue.advance_run 注释里「进程内只有一个推进线程」改成「同一运行只派一个工作线程」。
  • 跨进程互斥(RunnerLock,替掉每个节拍拿放一次的 runner_tick_mutex):PostgreSQL advisory lock(0x6F6D6D01)拿在一条单独的 AUTOCOMMIT 连接上(不留长事务、不挡 VACUUM),有步骤在途期间一直拿着,空闲的节拍才放——节拍很快返回而步骤还在跑,若照旧每个节拍放锁,另一个 API 进程就能抢到锁、把在途步骤判死重跑(双倍扣费)。停机时还有步骤在途就不放,留给进程退出、连接断开时释放。SQLite 只出现在测试夹具,直接放行。
  • 连接池(config.py / db.py / main.py):PostgreSQL 池放大到 database_pool_size = 10、database_max_overflow = 40(OMM_DATABASE_POOL_SIZE / OMM_DATABASE_MAX_OVERFLOW 可覆盖):每个在途运行要用连接,推进锁另占一条,再加 HTTP 请求、SSE 与对话轮,默认池(5 + 溢出 10)在几个任务同时跑时会让请求排队等连接。
  • 建任务闸门(routers/task_runs.py / config.py / schemas.py):default_max_concurrent_runs 3 → None(不限;部署仍可用 OMM_DEFAULT_MAX_CONCURRENT_RUNS 设默认上限),max_concurrent_runs_ceiling = 8 只约束用户自己选的上限。上限为空时整段闸门跳过;设了上限照旧只数排队中与执行中的运行,409 文案改成「把最大并发任务调高或改成不限」。PUT /api/account/preferences 的 max_concurrent_runs 改为必传、可为 null(= 不限,回落部署默认)——漏传字段是 422,不会把已存的上限悄悄清掉;OpenAPI 基线随之重导出。
  • 存量数据(迁移 0022_unlimited_concurrent_runs):旧面板每次保存都把下拉当前值一起推上去,而下拉默认显示「3 个」,所以存着的 3 绝大多数不是用户刻意选的(当天两个真跑的属主存的就是 3,不清的话照样被 3 卡住)。迁移把 max_concurrent_runs = 3 清回 NULL,1 / 5 / 8 这类要动手才选得到的值保留;纯数据修正,downgrade 不回写。本地默认链路启动只 create_all、不跑迁移,已有的开发库要手动 alembic upgrade head。
  • 前端:「最大并发任务」下拉加「不限」放第一个(默认),其余按 1 / 3 / 5 / 8 升序;说明文字改为「同时排队或执行中的任务数上限,默认不限、保存后立即生效;任务之间并行推进,同时跑得越多越抢本机算力和模型接口额度」(legacy/openmathmodel-ui.ts 只改这一行,英文词典同步)。account-preferences.ts 的 parseMaxConcurrency 改成三态:数字 / null(不限)/ undefined(认不出、不推送——不能和「不限」混用,否则一个坏值就把上限清掉了);回填支持 null。受保护入口、页面模板与路由零改动。
  • 并行的代价:并行的沙盒互相抢 CPU,单次运行的 120 秒时限更容易撞上;同一模型接口的并发请求更多,可能被限流变慢;月度费用的硬限制按步检查,并行时最多多花「并行任务数」步。
  • 测试:backend/api test_runner_lock.py 改写(runner_tick_mutex → RunnerLock:无竞争放行;PostgreSQL 上对手持锁时拿不到、放了能拿到;持锁连接是 idle 而不是 idle in transaction;锁被占时节拍不派活、放了恢复推进;步骤在途期间对手拿不到锁,步骤结束后也要等到空闲的节拍才放);新增 test_runner_parallel.py(两个运行的慢步骤同时在跑——串行必然超时;同一运行上一步没完不派第二个线程;一个运行抛错不连累别的、下个节拍再派;真引擎 + 真调度循环三个模拟运行一起推进到第一个闸门;连接池按配置放大);test_account_preferences.py 改写(默认不限且回填 null;漏传字段 422、不清旧上限;默认不限连建 10 个;设成 2 个时第 3 个 409,改回不限即放行);新增 test_concurrency_default_migration.py(0021 → 种 3 / 1 / 5 / 8 / NULL → head,只有 3 变成 NULL);前端 network-preferences.test.mjs +1(「不限」→ null),坏值断言改为 undefined。

当日执行并通过:临时 worktree 里 pytest backend/api/tests SQLite 572 passed / 5 skipped、PostgreSQL 576 passed / 1 skipped(advisory lock 与连接池用例只在 PostgreSQL 上跑);另起 worktree 检出改动前代码跑新测试:test_runner_lock.py 收集即 ImportError,test_runner_parallel.py 4 条 SQLite 用例全红(串行节拍卡在 barrier 上等到超时),偏好与迁移的 4 条新断言全红。与上一节的改动一起合回主工作区后的全量结果见上一节;开发库 alembic upgrade head(0021 → 0022)。验收(10-07,开发服务 + 测试账号,截图在 audit-current/2026-10-05-unlimited-concurrency/,不入库):设置中心「最大并发任务」回填「不限」,下拉为不限 / 1 个 / 3 个 / 5 个 / 8 个,与改动前截图同尺寸同取景;测试账号连建 5 个 auto_start=false 的运行全部 201 / QUEUED(第 4、5 个不再 409),核对完全部取消。未验:多个真实运行同时跑实验阶段时的 CPU 争用与模型接口限流;两个 API 进程共库时的锁交接只由 PostgreSQL 单测覆盖。要用上这次改动须重启 API,并对已有开发库跑一次 alembic upgrade head。

2026-10-07 沙盒一波回合用完、一次没运行不再判死:凭剩下的运行预算再开一波

10-05 宠物产业真跑(run_d14b2827)停在 FAILED,实验阶段两次都死在同一处。第 5 次实验(G3 推荐重做实验的那一轮)8 个回合 = 列目录、读两个数据文件、ws_write 五次重写 experiment.py(2.0k → 1.4k → 4.0k → 3.6k → 16.9k 字符)——模型以为 ws_write 能追加,把脚本分几段写,第 8 回合才写出完整脚本,回合就用完了;第 2 次实验(检验后自动回退那一轮)8 个回合全花在读文件上(检验结果、两个数据文件、上一轮 experiment.py 按 3000 字符分 4 段读完)。两次都是内环 max_turns 出口(E330),R2 的 6 次运行一次没动、还剩两波,run_sandbox_task 见内环没交出合法终答就收束,整个阶段判失败。ws_write 的实现是整文件覆盖,给模型的工具说明却只写「写工作区 UTF-8 文本文件。」;波次提示词只报 R2 还剩几次运行,不报每波只有 8 个回合。用户拍板:连着修真跑暴露的两个缺陷,先修这一个。

  • 执行体(agents/harness/src/omm_agent_harness/sandbox_agent.py):内环以 max_turns 收束(回合用完)、断言(含收尾运行)没过、没取消、R2 运行预算还有余额时,不再收束、照常开下一波——回合用完只是这一波的配额尽了,不是任务的预算尽了。下一波的反馈开头写明「上一轮 N 个回合用完也没有交出终答」:这一波一次没运行时再点名它调用了哪些工具(如 ws_read ×7),要求先把完整脚本直接交给运行工具(源码作为 code 传入,不必先 ws_write 落盘;ws_write 整文件覆盖、不是追加),不要把回合花在分段重读整份旧文件上;跑过但回合用完时带上运行次数,提醒跑通自查后就交终答。结构违约(E120)、无进展(E331)、工具连败(E332)与取消照旧直接收束(修复梯子不跨级);波数上限 max_waves = 3 照旧兜底,单波 8 回合、R2 = 6 都不改。
  • 回合可见:波次提示词「工作方式与终答要求」补一句「每一轮最多 N 个回合(调一次工具或交一次终答各算 1 个),回合用完还没交终答这一轮就到此结束,脚本写好就直接运行」。运行跟踪器按波计数——内环每个工具回合调一次执行器,调用次数就是本波已用的回合数;本波还没运行过、只剩不超过 2 个回合时,在这一回合最后一条观察前面加「[回合提醒]」(成功的观察加 turn_budget 键排在最前,失败的缀在报错后面)。加注只进模型的观察,原始结果照存,断言与节点看到的不变。
  • 工具说明(agents/skills/src/omm_agent_skills/chat_adapter.py):文本协议给模型的 ws_write 说明补「整文件覆盖写入,不是追加」。
  • 契约、OpenAPI 基线、节点与前端不动;报告的 attempts 照旧上报波次数(回合用完后开的那一波也算一波)。
  • 测试:harness test_sandbox_agent.py +8(真跑形状:第一波 8 个回合只读写不运行 → 第二波运行通过、attempts 为 2,反馈点名调用过的工具;跑过但回合用完的反馈带运行次数与断言差异;回合提醒在成功 / 失败两种观察上的写法;跑过之后不再提醒;波次提示词写明每波回合数;R2 尽或已是最后一波时照样收束;取消之后不再开新波;E331 / E332 照旧收束);skills test_nodes.py +1(实验节点:第一波 8 个回合只写不跑 → 第二波运行通过 → 节点成功、共两波)。

当日执行并通过(与下一节一起回归):agents 五包 + worker 900 passed / 1 skipped,pytest backend/api/tests 572 passed / 5 skipped(PostgreSQL 576 passed / 1 skipped);CI 口径的 Python 段(compileall、契约 validate.py / check_compat.py、generate_python.py --check / --verify、API import、export_openapi.py --check、六包 import)全过;strict mypy 本节两个文件与改动前逐条相同;ruff 对照改动前 E501 等规则零新增,只多了中文全角标点类提示(RUF001–003,全仓同款)。临时 worktree 检出改动前代码跑新测试:harness 4 条行为用例红、4 条守卫用例绿,节点用例红。未验:真实模型拿到「上一轮回合用完」反馈与回合提醒后的表现(用例全部打桩)——要在真实运行里用上,仍须把旧 API 进程整个停掉再启动。已知局限:单波仍是 8 个回合,读大文件仍花回合;一个任务最多 3 波,三波都只读写不运行仍会判失败。

2026-10-07 G3 跨轮对比对上号:上一轮未过的检查必须同 id 复检,或写明为什么不复检

检验节点在回退重做轮产出跨轮对比 round_comparison(上一轮未过的检查 × 本轮检查,按 id 求交分三桶:转为通过 / 仍未通过 / 本轮未复检;09-18 进契约、09-27 结果页渲染),G3 卡片的「较上一轮」句、论文材料与结果页都据此说话;但「沿用同名 id」只写在任务卡与审稿材料的提示词里。真跑 run_5d6bf571 第 3 轮检验的 15 项检查 id 全换了:forecast_target → forecast_target_consistency、pareto_front → pareto_nondominated、storage_param_sensitivity → storage_cost_robustness,另有两项换了说法,只有 data_provenance 是真的没再复检——上一轮未过的 6 项全落进「本轮未复检」,跨轮对比形同虚设。这是用户拍板连着修的第二个缺陷。修法不做按名字的模糊配对(配错就是编出来的事实,与契约「只计数与点名、不判好坏」冲突),改为验收时强制同 id。

  • 验收断言(agents/skills/src/omm_agent_skills/nodes.py):回退重做轮(上一轮检验真跑通且有未过项,与跨轮对比同一口径)给检验沙盒加一条断言 previous_checks_rechecked:上一轮每个未过的检查 id,要么在本轮标记行的 checks 里同 id 出现(检查实现可以改进,id 与阈值不变),要么在标记行 "not_rechecked": [{"id": …, "reason": …}] 里写明不复检的原因(缺 id 或缺原因不算声明);缺了就判不过、按 R2 打回修复,反馈点名缺了哪些(id + 名称)、本轮现有哪些 id、出口怎么写。首轮检验不加这条断言,任务卡不变。
  • 任务卡与审稿材料:任务卡「上一轮未过检查」段改为逐项沿用原 id 复检(验收会逐个核对)、阈值照旧、确实无法复检的写进 not_rechecked、不要改名或悄悄删掉;检验审稿材料的「上一轮反馈」段附上本轮声明不复检的条目与原因,事实行要求审稿人核查「声明不复检的理由是否成立」;检验审稿提示词模板 validating_review.default 升到 v4,第 6 条「跨轮核查」同样写明:声明不复检的项须核查理由是否成立,确实无法复检(如所需数据本轮拿不到)才算成立,站不住的按「删掉」记 major 并点名。
  • 跨轮对比:仍按 id 求交;落进「本轮未复检」的项若有声明,条目带 reason,G3「较上一轮」句与论文材料写成「名称:原因」(有原因时用「;」分隔,没有原因时逐字照旧)。experiment-summary.v1 的 round_check_ref 只有 id / name、API 投影只取这两键,结果页照旧——契约、OpenAPI 基线与前端不动。
  • 测试:skills test_nodes.py 改 2 条(test_validation_redo_compares_checks_across_rounds:本轮改为声明一项不复检,断言任务卡新文案、验收清单多出新断言、对比与 G3 句带原因、审稿卡带声明,对照组首轮的验收清单没有新断言;test_reviewer_redo_notes_carry_last_round_blockers_and_facts:事实行新文案),新 2 条(test_validation_redo_renaming_previous_checks_is_sent_back_for_repair:第一波把上一轮的 id 改了名被打回,第二波改回原 id 后三桶各就各位;test_previous_checks_rechecked_assertion_accepts_same_ids_or_declared_reasons:断言纯函数的四种情形);另给守卫用例 test_reviewers_receive_last_round_feedback_in_their_task_cards 补一条断言:检验审稿角色卡里有第 6 条的新文案。

当日执行并通过:见上一节(两节一起回归)。临时 worktree 检出改动前代码(只含上一节的改动)跑新测试:改 2 新 2 共 4 条红;守卫用例 test_reviewers_receive_last_round_feedback_in_their_task_cards 原有断言都过,只红在新补的模板断言上。strict mypy 较改动前多 2 条 no-untyped-def(新断言工厂与内层 check 沿用同文件既有断言工厂的无注解写法,同款)。未验:真实模型会不会拿声明出口躲开没过的检查——由检验审稿人按理由是否成立把关(模板第 6 条、事实行与材料附注三处同一口径);run_5d6bf571 那种整组改名的真跑复现。要在真实运行里用上,仍须把旧 API 进程整个停掉再启动。

2026-10-07 论文正文不再写原始指标名与十几位小数:写作提示词改用中文说法、4 位有效数字,数字审计认四舍五入

用户反馈论文页导出的 Word 排版问题,导出器那一侧已修(不插零宽空格、不开单词中间换行、重写表格列宽,见 ADR-0025 修订)。用本机 WPS 实测后仍有几行被两端对齐拉开:摘要里连写着 rolling_domination=0.0021355180250620664、forecast_target_consistency=0.9175215033197788 这类内部指标名和 16~19 位小数,Word / WPS 都断不开。根因有两条。一是实验脚本的 OMM_METRICS_JSON 只有「键 → 值」,冻结清单给实验指标的「含义」只写 实验指标 {key},写手没有别的叫法可用。二是 paper_section 要求冻结值「不四舍五入、不改精度」、paper_finalize 要求「保持原样」,因为数字审计按数字串逐字对账,写成 0.002136 会被记为无出处数值。用户拍板:提示词与审计一起改。

  • 数字审计(agents/skills/src/omm_agent_skills/frozen_numbers.py):unsourced_numbers 在允许集里找不到某个 token 时,再看它是否等于某个允许值(冻结值 ∪ 材料数值)按 token 自己的小数位四舍五入(ROUND_HALF_UP / ROUND_HALF_EVEN)的结果。只有至少 3 位有效数字的 token 才这样判:0.5、12 这类短数舍入后能对上太多值,仍只认相等。截断不算舍入。章级审计重写、摘要核对与终稿审计都走这个函数,口径一致。清单表头改为「保留 4 位有效数字四舍五入(附例),不换算单位、不改写成别的数;『含义』里的英文键名只用于对账,正文用中文说法」。
  • 写作提示词:paper_section(v7)、paper_finalize(v4)、paper_writing(总编失败时的整篇回退,v12)改为:数值保留 4 位有效数字四舍五入、不换算单位;指标和变量在正文、摘要、表格里都用中文说法(按材料里的定义写),不写 rolling_domination、metrics.xxx、data_source_synthetic=1 这类代码标识符或「键=值」写法;材料里看不出含义的指标宁可不写。paper_outline(v10):brief 点名指标时写中文说法,括号里附清单编号;编号和键名只供写手对账,不进正文。章级审计重写的反馈改为「改为清单 / 材料中的数值(可保留 4 位有效数字四舍五入,不得换算或另编)」。
  • 契约、OpenAPI 基线、前端不动;DocumentDraft 里的冻结清单照旧存全精度值。已生成的论文不受影响。
  • 测试:skills test_nodes.py 改 1 条(清单表头新文案)、新增 1 条(test_unsourced_numbers_accepts_values_rounded_to_their_written_precision:舍入到 6 / 5 / 4 / 0 位小数的都认;截断、不足 3 位有效数字、舍错方向的照报)。

当日执行并通过:agents 全量 857 passed / 1 skipped,其中章级审计重写用例里编造的 300、99、1180 照样被报。未验:真实模型按新提示词写出的中文指标名是否贴切(用例全部打桩)。要在真实运行里用上,须把旧 API 进程整个停掉再启动。已知局限:实验脚本仍不输出指标的中文名,写手只能从材料推断,材料里没有定义的指标会被略去。

2026-10-08 报障修复:论文阶段还没开始,论文页就摆着一整篇别的任务的论文(旧全局草稿键串台)

用户带图报障:「VLSI布图规划优化建模」(run_4b46e80…,10-03 在实验阶段失败,库里没有论文阶段产出)的论文页摆着一整篇「失联潜水器」论文(2 问题分析 / 2.1 约束分析…,底部「16,426 字 · 已恢复本机草稿」)。用户以为是写死的模板,要求论文正式开写前显示已有的加载动画。

根因:代码里没有这篇稿子(全仓零命中),它属于 09-09 的项目 proj_04371d1d…。legacy/openmathmodel-ui.ts 的 bindPaperEditor / savePaperDraftNow 把本机草稿存在不分项目的全局键 openmathmodelPaperDraft.v1:在任一任务的编辑器里动一下(打字、加粗、Ctrl+S)整篇正文就写进去,此后打开任何任务的论文页都会被灌回编辑器,状态栏显示「已恢复本机草稿」。有论文产出的任务里 renderEditorPanel 随后用定稿把它顶掉,所以看不出来;没有产出的任务里它就一直摆着,占位段落被顶掉,workflow-empty 的空态插画也挂不上。2026-09-17 那次「正文一万六千字、大纲是空态」的报障是同一篇串台稿,当时只补了大纲。task-autosave 与 hasUserPaperDraft 早已按项目分键,只有这把全局键在串台。串台期间若在别的任务页里点过编辑器,task-autosave 还会把这篇以 user_edited: true 存进那个项目的按项目键。

  • 前端 新增 tasks/paper-draft.ts(零依赖):readUserPaperDraft / writeUserPaperDraft / clearPaperDraft,按 URL 上合法的 project_id(否则落到 demo 档)读写 openmathmodel.paperDraft.v1.<scope>({html, saved_at, user_edited: true}),作用域推导只剩这一份。编辑器的 savePaperDraftNow / bindPaperEditor / resetPaperDraft、task-autosave 的落盘与恢复、stage-content 的 hasUserPaperDraft 全部改走它。全局键不再读写;「恢复初始正文」照旧连它一起清。
  • 串台副本:demoteLeakedPaperDraft() 在本项目记录与全局键内容一字不差时把记录改为 user_edited: false(正文留着不删),之后不再恢复、也不挡 Agent 正文。只在 renderStageContent 的 document_draft == null 分支调用(dropLeakedPaperDraft,排在 syncPaperOutlineFromEditor 之前);降级后编辑器换回占位段落,字数归零,状态回到「编辑内容自动保存到本机」,大纲回到「论文大纲将在论文生成后显示。」。有论文产出的项目一概不碰:全局键记的是最后一次编辑的地方,那里一字不差的那份多半就是用户在自己论文上的修改。代价:被污染的任务第一次打开时,旧稿会一直显示到阶段产出返回(夹具实测 46 ms),之后不再出现。
  • 不动:页面模板、workflow-empty.tsx 的挂载规则、直播与定稿渲染、.outline 结构、受保护入口。
  • 测试:新增 tasks/paper-draft.test.mjs 7 条:按项目隔离;全局键永不当草稿恢复,写草稿不碰全局键;非用户草稿、空正文、损坏记录一律忽略;清除只清本项目和全局键;一字不差的副本降级,正文与时间戳保留;用户自己的草稿(没有全局键 / 内容不同 / 在副本上改过)绝不降级;存储被禁用时不抛错。

当日执行并通过:npm run check;node --test "src/**/*.test.mjs" 248/248;npm run build(index 942.21 kB)。浏览器验收用本机夹具 .agents/paper-draft-leak-fixture.mjs(API 走夹具、页面转发 vite,localStorage 按夹具源隔离),截图在 audit-current/2026-10-08-paper-draft-leak/,不入库:

  • 改前:全局键里放一篇串台稿,打开实验阶段失败、没有论文产出的运行的论文页 → 整篇旧稿 +「已恢复本机草稿」+ 大纲按旧稿建,与用户截图同形。
  • 改后:同一页面显示水母空态「把探索写成论文 / 开始撰写」,0 字,大纲空态。按项目键里放一份与全局键相同的副本 → 第 140 ms 旧稿出现、第 186 ms 换回空态,记录降为 user_edited: false。
  • 点「开始撰写」输入 → 700 ms 后存进本项目键、全局键原样;刷新后恢复且不降级。换一个 project_id → 空态。「恢复初始正文」→ 空态,本项目键与全局键都被清掉。
  • 有论文产出的运行(run_e898… 的真实投影,.agents/dump-stage-outputs.py 只读导出):与全局键相同的用户草稿照常恢复、不降级;没有草稿时直接渲染定稿(19,872 字),全程不闪旧稿。
  • 运行页、数据、建模、实验、论文、完成 6 个路由加载均无脚本错误。

未验:用户真实浏览器里各项目键的污染情况(不读取用户浏览器数据)。应用内验收待用户:刷新截图里那个任务的论文页,应看到水母空态,左侧大纲为「论文大纲将在论文生成后显示。」。已知、本次未改:失败的运行论文页右上角仍写「Agent 正在撰写」(continue-paper 按钮只区分是否 COMPLETED)。

P1:新任务控制链(已落地,继续补端到端自动化)

  • 首页与确认页已使用现有 DOM 创建 Project/TaskRun;
  • 草稿携带任务类型、模型选择和附件元数据;
  • 返回的 run_id/project_id 已进入任务执行页;
  • 401 已打开现有登录模态并在成功后恢复动作;
  • 后续补充真实浏览器登录后的创建断言、重复点击幂等断言和二进制附件上传。

P2:阶段输出持久化

  • 增加 StageOutput 版本、哈希、来源 Artifact 与 producer step;
  • 保留历史版本;
  • STEP_SUCCEEDED 不再丢弃 outputs/metrics;
  • SSE 增加轻量 output-updated 指针事件。

P3:五类页面正文

按 DatasetProfile → PlanProposal → ExperimentSummary → DocumentDraft → DeliveryManifest 顺序实现。每类都要先发布 Schema、Fixture 和生成类型,再接 API 与 DOM。

P4:真实执行面

  • 用节点注册表替换 SIM_NODES;
  • 补齐 DATA/EXPERIMENT/VALIDATION/PAPER 技能;
  • 统一 API 与 backend/worker 的队列、租约、事件和 Artifact Store;
  • 保持本规范的 workspace 接口与页面合同稳定。