diff --git a/docs/decisions.md b/docs/decisions.md index af2107177..5b24d8b7b 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -41,7 +41,7 @@ | ADR-031 | Accepted 2026-09-04 | 分页 clamp 可观测性(#2243 残项):**保留「夹到端点自己声明的上限」,不改 400**。实测 13 个 list handler 的信封已回传 `page.nextCursor` + `page.hasMore`(另两个 clamp 端点是 limit/offset 形态,客户端仍可推进),所以被夹短的页**可续取、不是数据丢失**;400 会把可满足的请求变成硬失败、零用户收益,并与 `repository/pagination_clamp_test.go` 钉住的 `ClampPageSize` 不变量冲突。可观测性用**文档化**补齐(`api/conventions.md` + OpenAPI `PageSize` 描述写明「超过声明上限即夹到该上限,余下部分用 nextCursor/offset 续取」),**不**新增信封字段(跨 ~16 端点的契约变更换近乎零价值)。残项:`repository/message.go GetMessagesIncrement` 的非正值分支按 `paging.go` 自己写明的「0 = no explicit limit ⇒ 把 requested 当 def 传」惯例表达,行为不变、消掉最后一处手写分支。 | Hub API | 是 | | ADR-032 | Accepted 2026-09-04 | CI 反馈回路裁决(#2251)。**① 量化目标已达成**:`@agenthub/workbench` coverage job 从实测中位 **441s**(15 次 pre-#2300 run,自测基线而非沿用 7m31s 口径)降到 **259s**(5 次 post,**−41.3%**;把 #2300 自己 4 次 pre-merge run 并进来的 n=9 口径 −40.1%),机制在 CI 日志里验证过:vitest `tests` 桶 796.49s → 110.16s worker-seconds(−86%),达成并发度恒为 **3.86**(=job 内已无并行度可挖)。**② stretch ≤3m 不做**:单 runner 下限已是 870/3.86 + 30 = **255s**,要到 180s 得再删 ~35% 测试工作量,那是拿覆盖保障换 KPI。**③ workbench coverage 分片(`vitest --shard`)DEFERRED**:在 post-#2300 逐文件成本上重拟合 LPT+4worker 模型(N=1 标定 −2.0%),N=3/N=4 把该 job 降到 1m45s–2m26s,但 **PR 墙钟中位只从 289s → 275s(−4.8%),9 次里 4 次收益恰好为 0**——FE-only PR 已成 `frontend-required`(5/9) 与 `windows-frontend`(4/9) 的双极平局(中位差 20s);同时分片把 FE-only run 叶子数 15→19,而账号级并发上限实测 **~20**,两个重叠 FE-only PR 会 30→38(30 那一档本窗口内已饿过一次)。**触发重开**:该 job 墙钟中位 ≥ **~5m10s**(= 交叉点 ~4m09s + 60s 余量),或 ④(c) 落地之后。**④ 真正的极已换人**:(a) **`go-hub-test` 分片失衡——本 ADR 同批实施**:`internal/repository` 一个包 = hub-server 实测 418.8s race 测试成本的 **227.7s(54.4%)**,它同时是任何包粒度切分的**硬下限**;`NR % 2` 把它放在 `go list` 第 15 位(奇数)⇒ shard 2,而 shard 2 在 **14/14**(post-#2300 窗口 **18/18**)次 CI run 里都是慢的那半(中位 +119s / +2m02s,最大 +177s / 3m20s)。改成 shard 1 独占该包、shard 2 拿其余 51 个:load **310.5/108.3 → 227.7/191.1**(spread 202.3s → 36.5s),**不新增 job**;**本 PR 自己的 CI run 就是这次改动的实验,实测已回收**:`go-hub-test (1)`=**3m49s**、`(2)`=**3m28s**(spread **21s**),对照基线 shard1 2m01s–3m00s / shard2 4m41s–5m14s(中位 spread +119s)⇒ 矩阵跨度 **−62s**,落在预测的 −40…−70s 区间内;含 1 个包的那片反而成了较慢的一片(229s vs 208s),说明「测试二进制 link 成本随包数增长」这一项确实存在、量级约 20s,比 load 失衡小一个数量级,所以按 load 切是对的方向。同一 run 的 `go-edge-test` 未改:(1) 2m39s / (2) 1m48s,仍不是 `backend-required` 的极 ⇒ (b) 的「不动」判断被新数据再次支持。硬编码包路径不会静默腐烂——包被改名/移动/拆分则 shard 1 收到 0 个包,既有 0-package 守卫硬报错,`verify-ci-gates.py` 另加 3 条断言 + 2 发变异自测钉住。(b) **`go-edge-test` 保持轮转**:实测 edge 无单一支配包(最重 `internal/lifecycle` 34.57s = **24.0%**),当前 spread 29.1s load(shard1 重,与 CI「edge shard2 在 14/14 次里更快、中位 −46.5s」同向),LPT-2 只省 14.1s load,而 edge-test 从来不是 `backend-required` 的极(2m39s vs hub 4m51s)⇒ 省下的时间到不了 PR 墙钟;触发重开 = edge-test 成为 backend 极。(c) **`windows-frontend` 的 ~75s/leg 环境准备税 DEFERRED**(同样四步在 Ubuntu 上 21.5–24.0s:setup-node +28~29s、pnpm action-setup +19~22s、pnpm install +20~22s、checkout +5~6s,且 PRE→POST 还在变慢 3m54s→4m07s):只读测量量得出税额、量不出任何修法的效果,需要一次真实 CI 实验;**禁止**用砍掉 Windows leg 的 `Production build` 换那 47–50s——`desktop-linux-build` 是 `workflow_dispatch` only(43 次 run 出现 0 次),Linux `frontend-desktop` 只跑 typecheck+lint+test 不 build,那条腿是每个 PR **唯一**证明 desktop `tsc -p tsconfig.app.json && vite build` 能过的地方。(d) 两个前端杠杆**严格互补、单独都不划算**:Windows 税单独修 = **+0.0%**(`frontend-required` 变成 9/9 的极)、分片单独 = −4.8%、两个一起 = 4m49s → **3m49s(−20.5%)**、再加 `frontend-desktop` 分片重排(164s vs 138s)= **3m29s(−27.3%)**。**⑤ 测量方法学(防下次重推)**:本机 ARM64 上 `go test -short -count=1 -p 1` **不带 `-race`** 得到的成本分布与 CI 完全不同(repository 只占 9.9%、`internal/middleware` 占 31%),带上 `-race` 才复现 CI 的方向;两个模块的失衡方向都被 **28/28** 次 CI 观察独立证实 ⇒ 任何 shard 重排必须用 CI 同款 flag(`-short -race -count=1`)逐包测,命令:`go test ./... -count=1 -short -race -p 1 -parallel 1`。证据底稿 `lane-artifacts/round-73/lane-ci-REPORT.md` 与 `lane-ci-post2300-REPORT.md`。 | CI / Developer feedback loop | 是 | | ADR-033 | Accepted 2026-09-04;landed #2315 | regenerate 的 identity 合同(#2274 B-1):**identity = task id**(服务端合同 `POST /web/agent-tasks/:id/regenerate` → `RegenerateAgentTask(userID, taskID)` 不变)。真流取证(live 栈 + 真 OIDC)证明修复前 web 把 block id 剥前缀后当 task id 发——而 block id 实为消息的 **client_msg_id**(第三个 identity 域),live 恒 404 `agent_task_not_found`、未登录 demo 对真后端发无凭据请求 401。修法三段:① **生产端补齐**——hub 两条 edge 回调路径(stream 投影 + done-final)在 agent 消息 content jsonb 里 stamp `agent_task:{"task_id":…}`,即 shared normalizer 早已解析却 0 生产者的形状(demo fixture 是唯一生产者);非 object content 不 stamp、既有 ref 不覆盖。② **transcript 写穿**——`normalizeHubMessages` 把它写成 `block.agentTaskId`(`TextTranscriptBlock` 新增可选字段)。③ **双重诚实门**——菜单条目与 regenerate effect 同时要求「端口已接线」**且** `block.agentTaskId` 存在;web 的 `onRegenerate` 随 `chatActions` fail-closed(demo/未登录端口为 undefined ⇒ 入口不渲染、零请求)。**禁止**:从 message id / client_msg_id 猜 task id、失败后静默 fallback、demo 向真 API 发请求。**历史消息无 stamp ⇒ 入口不出现**(fail-closed,不做回填迁移)。验收三层:contract(Go 单测 + live API 404/200 对照)、frontend(shared/workbench/web 单测含两个诚实门用例)、real flow(真浏览器点击:PRE 404+失败 toast / POST 200+新任务+成功 toast;demo PRE 401 请求 / POST 零请求)。证据 `lane-artifacts/round-74/b1-*-PRE/POST.json` + 截图。 | Frontend / Hub dispatch | 是 | -| ADR-034 | Accepted 2026-09-04(卫生切片 landed;语义部分 DEFERRED 待 operator) | 项目状态筛选的 vocabulary 裁决(#2274 B-6)。**事实**:Hub 的 workspace/project **没有 status 事实**——model/service/handler/openapi/migration/live DB 六处逐字一致(live 表仅 6 列);`GET /web/projects` 也不接受 status 查询;UI filter(all/running/completed/archived)**100% 前端内存过滤**;三个同名 `workspaceProjectToProjectInfo` 产 5 个输出值,与 Hub 侧对应物交集为 ∅;匹配规则还有三套互相矛盾的实现(精确表 / 精确对 / 子串 `includes('归档')`,后者与表头注释明确拒绝的做法相反)。**裁决**:不在没有 L3 事实源的前提下发明 vocabulary;收敛必须三段——L3 Hub 真 lifecycle 事实(语义由 operator 定义:running/completed/archived 各由什么派生)→ L2 唯一映射(回到 `hubDataMapping.ts`)→ L1 类型分离(`statusLabel` 与 `lifecycle: bucket|'unknown'` 拆开,`unknown` 时 chips 不渲染)。**本批已做(不需裁决)**:删 0 消费者孤儿 mapper(#2291 后唯一「消费者」是测试里一句错误注释,主机已复核 desktop 只 import 编排函数、mapper 用本地副本)+ 修正该注释 + openapi 枚举对齐 0074(run status 补 `pending_review` ×2、assignment type 补 `compete` ×3;漂移根因是 3 个 verifier 只比路由形状不比字段集/enum)+ 清 workbench vitest coverage 对已删文件的 stale exclude。**DEFERRED 触发条件**:operator 给出 lifecycle 语义定义,或 Hub 获得 status 字段;在此之前 filter 词汇保持现状但**不得**在对外文档中被表述为 Hub 事实。证据 `lane-artifacts/round-74/b6-vocabulary-survey.md`(526 行,主机复核 3/3 关键断言成立)。 | Product / Frontend / Hub | 是 | +| ADR-034 | Accepted 2026-09-05;landed [#2323](https://github.com/TokenDanceLab/AgentHub/pull/2323) | Project lifecycle 只由服务端契约定义,不从显示文案推断。**事实**:Hub 的 Project/Workspace 当前没有 lifecycle/status 字段或转换 API;`GET /web/projects` 不接受 status 筛选。**裁决**:Workbench 不提供 Running/Completed/Archived 项目筛选,也不保留基于 `ProjectInfo.status` 显示标签的本地分类链;不能为了保留旧 UI 发明后端状态机。项目搜索、选择、CRUD 与 threads/messages 保留,Task、ProjectRun、queue 和 AgentTeam 的真实状态不受影响。**后续边界**:只有产品确需 Project lifecycle 且 Hub 提供对应 schema/API/转换语义时,才引入唯一映射和类型明确的筛选;在此之前,显示标签不是 lifecycle 事实,也不要求额外的标签类型重构。该裁决取代此前“保留筛选、等待 vocabulary 裁决”的暂缓方案;原始修复与验收记录见 #2316、#2323。 | Product / Frontend / Hub | 是 | | ADR-035 | Accepted 2026-09-04;landed #2317 | `web-v4` 键家族收敛(#2261 残项 = ADR-029 的适用范围正式扩展到 Web 自有命名空间)。**① 唯一产出点**:`webQueryKeys` 与 `hubQueryKeys`/`edgeQueryKeys` 并列住在 `queryKeys.ts`,`app/web/src` 非测试代码里的键数组字面量 **58 → 0**;**键值逐字不变**,只搬产出点,所以缓存身份与既有断言键值的测试都不动。**② 可空指针原样穿过**(`QueryKeyPointer = string|null|undefined`),**禁止**归一化成 `''`:`webHubMessagesFamily.sessionIdOf` 只判 `typeof key[2] === 'string'`,一个 `''` 会被重连补发当成真会话 id —— 那会把死键 bug 换成更糟的活 bug。**③ 失效必须打在真有生产者的键上**:据此修掉 6 类空转(联系人列表 6 处、新建群聊 1 处、登录后刷新 1 处、realtime 联系人帧 1 处、裸 `['agent-teams']` 1 处、以及一条断言了事实错误键的注释)。其中联系人列表有明确用户可见后果:该查询只有 `staleTime`、**没有 `refetchInterval`**,接受好友请求后列表会一直陈旧到窗口重新聚焦。裸 `['agent-teams']` 选择 **retarget 而非删除**——审批决定确实会改变 usageBoard 聚合的 runs,原作者意图对、键写错。**④ 无生产者的失效一律删,不留孪生键**(普查 `app/web`+`app/workbench`+`app/shared` 全量 `queryKey:` 生产点;workbench 0 处使用 react-query 键,故无隐藏生产者):Web 无通知查询 ⇒ NOTIFICATION_EVENTS 分支与事件集合整体删除;Web 只有 usageBoard ⇒ TEAM_EVENTS 6 条收敛为 1 条 family root,并删掉**只为拼死键而存在**的 teamId/teamRunId 解析;execution-targets 的 Web 孪生键删除。**⑤ 断言必须看缓存效果、不看调用**:原 `toHaveBeenCalledWith(['web-v4','execution-targets'])` 正是让 bug 活下来的护身符(键无生产者,套件却常绿);新增 `webQueryCacheTargets.test.ts` 以 `getQueryState(...).isInvalidated` 为准,**7 发变异 6 发翻红**,M6(把已删的通知分支放回去)**如实登记为不翻红**——它证明的是该分支本来就惰性,不是证明删除必要。 | Frontend | 是 | ## Archive diff --git a/scripts/verify/i18n-callsite-baseline.json b/scripts/verify/i18n-callsite-baseline.json index ceda44d65..8516296d1 100644 --- a/scripts/verify/i18n-callsite-baseline.json +++ b/scripts/verify/i18n-callsite-baseline.json @@ -33,7 +33,7 @@ "workbench/inspector/InspectorModeBodies.tsx": 3, "workbench/inspector/InspectorTabChrome.tsx": 1, "workbench/inspector/RuntimeEvidenceParts.tsx": 3, - "workbench/mockData.ts": 106, + "workbench/mockData.ts": 105, "workbench/pages/agents/AgentEditHelpers.ts": 2, "workbench/pages/agents/AgentInstalledParts.tsx": 9, "workbench/pages/agents/AgentMarketHelpers.ts": 2, @@ -54,11 +54,11 @@ "workbench/pages/docs/shared.tsx": 5, "workbench/pages/projects/ProjectChromeViews.tsx": 1, "workbench/pages/projects/ProjectTabViews.tsx": 10, - "workbench/pages/projects/shared.tsx": 16, + "workbench/pages/projects/shared.tsx": 10, "workbench/pages/projects/types.ts": 23, "workbench/pages/settings/SettingsPaneHelpers.ts": 11, "workbench/pages/settings/SettingsPaneParts.tsx": 4, - "workbench/pages/settings/SettingsPanes.tsx": 56, + "workbench/pages/settings/SettingsPanes.tsx": 55, "workbench/pages/settings/types.ts": 22, "workbench/pages/tasks/TaskTableHelpers.ts": 9, "workbench/pages/tasks/TaskTableViews.tsx": 1,