fix(regenerate): web「重新生成」改用真 task identity(hub 补 agent_task 生产端 + 双重诚实门) - #2315
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
DeliciousBuding
enabled auto-merge (squash)
September 4, 2026 04:51
DeliciousBuding
force-pushed
the
fix/web-regenerate-task-identity
branch
from
September 4, 2026 05:47
d7657dc to
0d0d9bd
Compare
…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
force-pushed
the
fix/web-regenerate-task-identity
branch
from
September 4, 2026 06:51
0496640 to
40991ed
Compare
…的 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>
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
修什么(#2274 B-1,round-74 Product Truth 主线第一条)
web「重新生成」在 live 模式恒失败、在未登录 demo 模式对真后端发空气请求。真流取证(live 栈 + 真 OIDC 登录 + 真 Hub 数据)证明根因是三层 identity 混淆:
hubReady/chatActions门约束,其它 5 个动作都受);hub-message-前缀后当 task id 发,而 block id 实为消息的 client_msg_id(hubMessageBlockId = client_msg_id ?? id ?? message_id)——第三个 identity 域;POST /web/agent-tasks/:id/regenerate→RegenerateAgentTask(userID, taskID)查pending_agent_tasks主键 ⇒ live 恒 404agent_task_not_found;demo/未登录则 401auth_invalid_token。PRE 证据(真浏览器抓包 + 截图,
lane-artifacts/round-74/b1-*-PRE.json):POST /web/agent-tasks/01a06a27-c8a6-7498-…/regenerate(client_msg_id)→ 404 → toast「Regenerate failed, please retry」;POST /web/agent-tasks/builder-reply-1/regenerate,Authorization 缺失 → 401。合同裁决(ADR-033,落
docs/decisions.md)identity = task id(服务端合同不变);前端不得猜。三段修法:
agent_task:{"task_id":…}—— 正是 shared normalizer 早已解析(agentTaskFromRecord)却 0 生产者的形状(此前唯一生产者是 demo fixture)。agentevent.StampAgentTaskRef:非 object content 不 stamp、既有 ref 不覆盖、空 task id/非法 json 原样返回。normalizeHubMessages→block.agentTaskId(TextTranscriptBlock新增可选字段,exactOptional:缺席而非 null)。block.agentTaskId存在;web 的onRegenerate随chatActionsfail-closed(demo/未登录端口 undefined ⇒ 入口不渲染、零请求)。端口签名(blockId, taskId)。历史消息无 stamp ⇒ 入口不出现(fail-closed,不做回填迁移)。
验收(三层,均有实跑证据)
agentevent4 发单测(object/array/scalar/既有 ref/空值)+ stream 投影断言 content 逐字含agent_task+ done-final sqlite 断言;live API 对照regenerate(message id)=404/regenerate(task id)=200+新任务。POST /web/agent-tasks/01a06a87-2c07-…/regenerate→ 200 + 新任务 + toast「Regenerating」;demo PRE 401 请求 / POST 入口消失、零请求。证据lane-artifacts/round-74/b1-*-POST.json+ 截图。门禁
本地复跑 checks.yml
validatejob 全 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:真栈实测 4 passed,
manifest-20260904-141958.jsonstatus=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_taskref。多出来的这个键正是本次改动的目的,所以整值匹配在 stamp 落地那一刻必然失败——它断言的是实现细节(落库内容逐字等于 edge 发来的内容),不是契约。这条门禁做了它该做的事:它抓住了一个真实的行为变化,并要求把这个变化显式写进契约。改法:
seq_id最大的一条」,这本身就是最终消息的定义;text字符串 +agent_task对象),解码类型map[string]string→map[string]json.RawMessage;content_type=text、text逐字相等);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),证明它不是空断言。