Skip to content

fix(regenerate): web「重新生成」改用真 task identity(hub 补 agent_task 生产端 + 双重诚实门) - #2315

Merged
DeliciousBuding merged 3 commits into
masterfrom
fix/web-regenerate-task-identity
Sep 4, 2026
Merged

fix(regenerate): web「重新生成」改用真 task identity(hub 补 agent_task 生产端 + 双重诚实门)#2315
DeliciousBuding merged 3 commits into
masterfrom
fix/web-regenerate-task-identity

Conversation

@DeliciousBuding

@DeliciousBuding DeliciousBuding commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

修什么(#2274 B-1,round-74 Product Truth 主线第一条)

web「重新生成」在 live 模式恒失败、在未登录 demo 模式对真后端发空气请求。真流取证(live 栈 + 真 OIDC 登录 + 真 Hub 数据)证明根因是三层 identity 混淆

  • 菜单条目无条件渲染(不受 hubReady/chatActions 门约束,其它 5 个动作都受);
  • 端口把 block id 剥 hub-message- 前缀后当 task id 发,而 block id 实为消息的 client_msg_idhubMessageBlockId = client_msg_id ?? id ?? message_id)——第三个 identity 域;
  • 端点 POST /web/agent-tasks/:id/regenerateRegenerateAgentTask(userID, taskID)pending_agent_tasks 主键 ⇒ live 恒 404 agent_task_not_found;demo/未登录则 401 auth_invalid_token

PRE 证据(真浏览器抓包 + 截图,lane-artifacts/round-74/b1-*-PRE.json):

  • live:POST /web/agent-tasks/01a06a27-c8a6-7498-…/regenerate(client_msg_id)→ 404 → toast「Regenerate failed, please retry」;
  • demo:POST /web/agent-tasks/builder-reply-1/regenerateAuthorization 缺失401

合同裁决(ADR-033,落 docs/decisions.md

identity = task id(服务端合同不变);前端不得猜。三段修法:

  1. 生产端补齐:hub 两条 edge 回调路径(stream 投影 + done-final)在 agent 消息 content jsonb 里 stamp agent_task:{"task_id":…} —— 正是 shared normalizer 早已解析(agentTaskFromRecord)却 0 生产者的形状(此前唯一生产者是 demo fixture)。agentevent.StampAgentTaskRef:非 object content 不 stamp、既有 ref 不覆盖、空 task id/非法 json 原样返回。
  2. transcript 写穿normalizeHubMessagesblock.agentTaskIdTextTranscriptBlock 新增可选字段,exactOptional:缺席而非 null)。
  3. 双重诚实门:菜单条目与 regenerate effect 同时要求「端口已接线」 block.agentTaskId 存在;web 的 onRegeneratechatActions fail-closed(demo/未登录端口 undefined ⇒ 入口不渲染、零请求)。端口签名 (blockId, taskId)

历史消息无 stamp ⇒ 入口不出现(fail-closed,不做回填迁移)。

验收(三层,均有实跑证据)

  • contractagentevent 4 发单测(object/array/scalar/既有 ref/空值)+ stream 投影断言 content 逐字含 agent_task + done-final sqlite 断言;live API 对照 regenerate(message id)=404 / regenerate(task id)=200+新任务
  • frontend:shared normalizer 2 发、workbench chrome 4 发(含两个诚实门)、web App 3 发(taskId 断言 + 无 stamp 无入口 + 无 chatActions 无入口);三包 typecheck 绿;web vitest 270/270;desktop 全量 374 文件 / 4979 例绿。
  • real flow(Goal 的硬要求):真浏览器点击 —— PRE 404+失败 toast / POST POST /web/agent-tasks/01a06a87-2c07-…/regenerate → 200 + 新任务 + toast「Regenerating」;demo PRE 401 请求 / POST 入口消失、零请求。证据 lane-artifacts/round-74/b1-*-POST.json + 截图。

门禁

本地复跑 checks.yml validate job 全 62 条命令:PASS=62 FAIL=0(runner 由 workflow 逐条重建,含 actionlint / secret guard --range / commit-message gate / 各 verifier 自测)。

Refs #2274 (B-1)

追加 commit:Golden Flows(GF2 = 本修复的回归门)

app/e2e/golden-flows.spec.ts,已注册进 app/web/playwright.real.config.ts 的 testMatch:

  • GF1 真登录 → 主界面拿到真实 Hub 数据;
  • GF2 真浏览器点击「重新生成」→ 断言请求 URL 含真 task id 且返回 200 + 成功 toast(这一条就是 B-1 的永久回归门);
  • GF3 demo/未登录 → 入口不渲染且零请求(诚实门);
  • GF4 edge 回调后 transcript 自动刷新。

真栈实测 4 passedmanifest-20260904-141958.json status=passed。实现要点:beforeAll 单次 ID 登录派生 web+desktop 会话、describe serial、登录 429 自愈(TokenDance ID 对 /api/auth/login 按 IP 限流)。

追加 commit:修好被 content stamp 打红的集成测试(并升级成 stamp 的 DB 级证据)

CI 的 Backend integration (PostgreSQL + Redis) 判红,本机真 PostgreSQL 复现一致:backend_e2e_hub_edge_contract_test.go:107 load final agent message: record not found

根因不是 stamp 错了,是那条断言的方式错了。 该用例按「整值相等」定位最终 agent 消息(WHERE content = finalContent)。content 是 jsonb,比较是语义级的(不吃空格与键序),但仍是整值匹配;而本 PR 之后落库的值 = edge 传来的 final_content 加上 agent_task ref。多出来的这个键正是本次改动的目的,所以整值匹配在 stamp 落地那一刻必然失败——它断言的是实现细节(落库内容逐字等于 edge 发来的内容),不是契约。这条门禁做了它该做的事:它抓住了一个真实的行为变化,并要求把这个变化显式写进契约。

改法:

  • 定位改为「该会话该 agent 实例 seq_id 最大的一条」,这本身就是最终消息的定义;
  • content 现在是混合形状(text 字符串 + agent_task 对象),解码类型 map[string]stringmap[string]json.RawMessage
  • 语义断言全部保留(content_type=texttext 逐字相等);
  • 新增契约断言agent_task.task_id == task.ID。这是 B-1 identity 合同的 DB 级一半(另两半是 transcript normalizer 单测与真实 Web→Hub 流)。

证据:本机真 PostgreSQL 该用例 PRE 复现 record not found、改后 PASS;全量 go test -tags integration ./tests/integration/ -count=1 = ok 82.073s、rc=0。变异测试 1 发(M8):把 StampAgentTaskRef 短路成直接 return content ⇒ 新断言翻红且诊断精确(decode stamped agent_task ref: unexpected end of JSON input),证明它不是空断言。

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 5cbc8cd8-ca3a-408d-8fef-73487749a044

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

DeliciousBuding and others added 2 commits September 4, 2026 14:37
…anscript 写穿 agentTaskId、菜单与端口双重诚实门

真流取证(live 栈 + 真 OIDC,lane-artifacts/round-74/b1-*-PRE.json + 截图)证明修复前:
- 右键真 agent 回复 → 菜单无条件提供「重新生成」→ 点击发
  POST /web/agent-tasks/<X>/regenerate,X 是 block id 剥前缀后的值,而 block id
  实为消息的 client_msg_id(hubMessageBlockId = client_msg_id ?? id ?? message_id)
  ——第三个 identity 域;端点查 pending_agent_tasks 主键 ⇒ live 恒 404
  agent_task_not_found,用户看到「Regenerate failed, please retry」;
- 未登录 demo 模式同样提供入口并对真后端发无凭据请求 ⇒ 401 auth_invalid_token
  (对空气发真实请求)。

合同裁决(ADR-033):identity = task id,服务端合同不变;前端不得猜。三段修法:
1. 生产端补齐:hub 两条 edge 回调路径(stream 投影 + done-final)在 agent 消息
   content jsonb 里 stamp agent_task:{"task_id":…} —— 正是 shared normalizer 早已
   解析(agentTaskFromRecord)却 0 生产者的形状(此前唯一生产者是 demo fixture)。
   agentevent.StampAgentTaskRef:非 object content 不 stamp、既有 ref 不覆盖、
   空 task id / 非法 json 原样返回(用户可见 payload 永不变形)。
2. transcript 写穿:normalizeHubMessages 把它写成 block.agentTaskId
   (TextTranscriptBlock 新增可选字段,exactOptional 风格:缺席而非 null)。
3. 双重诚实门:菜单条目与 regenerate effect 同时要求端口已接线且
   block.agentTaskId 存在;web 的 onRegenerate 随 chatActions fail-closed
   (demo/未登录端口 undefined ⇒ 入口不渲染、零请求)。端口签名改为
   (blockId, taskId),执行器把两者都交给 shell。

历史消息无 stamp ⇒ 入口不出现(fail-closed,不做回填迁移)。

验收三层:
- contract:agentevent 4 发单测(object/array/scalar/既有 ref/空值)+ stream 投影
  断言 content 逐字含 agent_task + done-final sqlite 断言;live API 对照
  regenerate(message id)=404 / regenerate(task id)=200+新任务。
- frontend:shared normalizer 2 发、workbench chrome 4 发(含两个诚实门)、
  web App 3 发(taskId 断言 + 无 stamp 无入口 + 无 chatActions 无入口);
  三包 typecheck 绿、web 270/270、desktop 全量 374 文件 4979 例绿。
- real flow:真浏览器点击 PRE 404+失败 toast / POST 200+新任务+「Regenerating」;
  demo PRE 401 请求 / POST 入口消失零请求(b1-*-POST.json + 截图)。

Refs #2274 (B-1)

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
不造新框架:复用既有 L3 real lane(playwright.real.config.ts testMatch 注册 +
run-real-e2e-lane.sh),全程真实 OIDC Authorization Code + PKCE(无自签 JWT、无
stub hub),agent 回复经真实 Edge→Hub 回调契约产生(唯一非真实部件是模型 runner)。

- GF1 真登录后主界面拿到真实 Hub 数据(会话标题与 agent 回复文本逐字来自
  provisioning,非 demo fixture);
- GF2 「重新生成」发**真 task id** 且 200 + 成功 toast(#2274 B-1 的回归门:修复前
  这里是 message/client_msg id + 404);
- GF3 未登录 demo 不提供会失败的「重新生成」且零 /regenerate 请求(诚实门回归);
- GF4 Edge 回调落地后 transcript 无需手动刷新(round-73 invalidation 修复的回归门)。

实测(huawei-dev 真栈,manifest-20260904-141958.json status=passed):
4 passed(GF1 17.3s / GF2 1.2m / GF3 11.3s / GF4 16.8s)。

两条环境事实已写进 spec 注释:ID 对 /api/auth/login 按 IP 限流(窗口内第 3 次起
429)⇒ describe 串行 + 登录按 Retry-After 退避自愈;create-session 响应键是
session_id、send-message 是 message_id(与 hub DTO 逐字对齐)。

Refs #2274 (B-1 回归门)

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@DeliciousBuding
DeliciousBuding force-pushed the fix/web-regenerate-task-identity branch from 0496640 to 40991ed Compare September 4, 2026 06:51
…的 DB 级证据

CI 上 Backend integration (PostgreSQL + Redis) 判红,本机复现一致:
backend_e2e_hub_edge_contract_test.go:107 load final agent message: record not found。

根因不是 stamp 错了,是测试的断言方式错了。该用例按「整值相等」找最终 agent 消息:
WHERE content = finalContent。content 是 jsonb,比较是语义级的(不吃空格与键序),
但仍然是整值匹配;而 B-1 之后落库的值 = 回调传来的 final_content 再加上
agentevent.StampAgentTaskRef 盖的 agent_task ref。多出来的这个键正是本次改动的目的,
所以整值匹配在 stamp 落地的那一刻必然失败——它断言的是实现细节(落库内容逐字等于
edge 发来的内容),不是契约。

改法:
- 定位改成「该会话该 agent 实例 seq_id 最大的一条」,这本身就是最终消息的定义;
- content 现在是混合形状(text 是字符串、agent_task 是对象),解码类型从
  map[string]string 换成 map[string]json.RawMessage,否则解码本身就报错;
- 语义断言保留:content_type=text、text 逐字等于 "Hub Edge callback contract final";
- 新增契约断言:agent_task.task_id 必须等于 task.ID。这是 B-1 那条 identity 合同的
  DB 级一半(另一半是 transcript normalizer 与真实 Web→Hub 流,已在 PR 正文的
  PRE/POST 真流证据里)。

证据:
- 本机真 PostgreSQL:该用例 PRE 复现 record not found,改后 PASS;
  全量 go test -tags integration ./tests/integration/ -count=1 = ok 82.073s、rc=0。
- 变异测试 1 发(M8):把 StampAgentTaskRef 直接 return content 短路掉 ⇒ 新断言翻红
  且诊断精确(decode stamped agent_task ref: unexpected end of JSON input,
  content={"text": "Hub Edge callback contract final"}),证明它不是空断言。

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@DeliciousBuding
DeliciousBuding merged commit 9cdc87a into master Sep 4, 2026
47 checks passed
@DeliciousBuding
DeliciousBuding deleted the fix/web-regenerate-task-identity branch September 4, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant