fix(voice): speak first without loosening truth or question-slot rules #48

Open
jesse wants to merge 0 commits from codex/agent-voice-20260901 into staging
Owner

Summary

Agent 可见输出改成先说人话,再守红线。基线是当时的 origin/staging @ 8617eb56;提交前已并入当前 origin/staging8b7eda9f,skill-parity 任务书)。顺序:任务 0 对照 → A → B → C → E → D。

  • 咨询:voice 置顶;本命仍是 3–6 句口语开场 + 不变的 Level 2 标题顺序。general / window / onboarding 只拿人设,不套本命骨架。
  • 校正:正文 2–4 句承接;硬编码用户文案集中到 frontend/src/lib/rectification-agentic/user-copy.ts
  • E-1:opening + collect + 本轮已有非空 assistant 正文时,focus 仍持久化,但不再把题干写成第二条助手消息。判定只看 action / kind / intent / 正文是否非空,不读正文内容。
  • E-2:引擎 user_meaning 仍给模型/审计;用户可见面走 engineMeaningToDisplayCopy。Python 引擎字段未改。skill 仍是 10.0.14。

任务 0 · 事故 case f83d9b42 改前 / 改后

形状:Skill 10.0.14;开场窗 04:45–05:15(30 分钟)收到 05:00–05:07(7 分钟);代表分钟 05:00。改前串按 8617eb56 硬编码 + route 直出复排,冻在 ACCIDENT_CASE_BEFORE_COPY。改后由 accidentCaseHardcodedTurns 从 copy 模块重建。

校正 assistant 串

改前 改后
开场采集(第二条独立消息) 请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。 不再追加第二条消息。 focus 仍是同一句采集题,由问题槽渲染。模型正文自己承接。
区分题转向 请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。 直接点下面的选项就行,打字回答也一样算数。
未对准当前题 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。
题已换 当前问题已更新,请刷新后重新作答。 这一问刚换成新的,刷新后再答就行。
中间轮区间 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。眼下更像 05:00。
穷尽后财务采集 有没有记得住时间的收入变化、大笔支出或欠债? 钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗?
交付/采用 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 可以从下面的时间里选一个采用。 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。代表分钟是 05:00。这只是代表性候选,不是已确认的唯一出生分钟。 可以从下面选一个先用着。

对照里开场不再出现语义重复的两连问(E-1)。

婚姻主题咨询开场三段

改前: 第一眼就是 ## 统一参数与原始结构,然后 ## 关系,文末 ## 技法审计表

改后: 先 3–6 句无标题口语(方向 + 一句承接),再 ## 统一参数与原始结构## 关系## 技法审计表(Level 2 顺序不变)。

红线 1 · 真实性 / 边界语义

  • 技法审计表仍要求写在本命正文文末(natalSpokenReportContract + consult user turn)。
  • answer_policy / precise-timing 守卫未动。
  • 交付/采用轮必须含「不是已确认的唯一出生分钟」语义:containsBoundarySemantics + REPRESENTATIVE_MINUTE_DISCLAIMER。事故 case 交付句见上表。
  • 中间轮允许省略完整边界句(见上表 intermediateRange)。
  • 证明:frontend/tests/agent-voice-copy-contract.test.ts(delivery/adopt 语义、中间轮不含完整边界句);rectification-provisional-adopt.test.ts B2;rectification-answer-choice.test.ts 采用叙述。

红线 2 · 问题槽归属

  • 服务端 focus 仍是唯一问题源。正文指令仍是「不得提问、复述、改写或拼接题干」。放宽的只是 2–4 句承接。
  • 选择卡仍由结构化槽位承载。GENERIC_COLLECT_QUESTION 仍由 spokenFollowupForUser / focus.prompt 写出,不由模型自拟。
  • 证明:rectification-server-focus.test.tsrectification-decision-authority.test.ts invariant 3(answer_choice 仍把 p_assistant_message === currentQuestion.prompt 写入历史)、agent-voice-copy-contract.test.ts 的 prompt 源扫描。

任务 E-1 · 结构化判定(不得用正文正则)

shouldPersistFocusPromptTurn 为 false 仅当

action === "opening" 且 (kind === "collect_spoken"intent === "collect_method_evidence") 且 assistantBodyPresent === true

矩阵(测试锁定):

action kind intent body 追加题干消息
opening collect_spoken collect_method_evidence
opening collect_spoken collect_method_evidence
opening choice distinguish_candidates
message collect_spoken collect_method_evidence
answer_choice choice distinguish_candidates

源码证明:shouldPersistFocusPromptTurn 函数体没有 .test( / .match( / .search( / includes(。stream 路径把 assistantBodyPresent: Boolean(result.answerText?.trim()) 传入,只问「有没有非空正文」,不问正文说了什么。BUG-467 记在 docs/BUG_HISTORY.md(复发自 BUG-460;不得用正文字符串判断「是否已问过」)。

任务 E-2 · user_meaning 分层

用户可见(已改走 display copy):

  • 盘面 stage / lagna contrast / dasha ledger / dasha agreement / nakshatra(rectification-board.tsx
  • 确认门 blocker(ConfirmationGateDisclosure
  • distinguish event_quality 选择卡 prompt(choice-card.ts
  • prospectiveWindowsNarration / hostNarration 附加句

仍进模型/审计(原样,含「不得…」):

  • sessionOutcomeViewbuildNextUserAction、skill verification report、probe briefs、Python 引擎 user_meaning

引擎字段未删、未改语义。

文案 旧 → 新 → 保留的语义约束

约束
请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。 从你最容易想起来的开始就好——比如哪年上的大学、哪年换的工作,记得大概年份就行。 仍是口述采集;不改题干所有权
有没有记得住时间的收入变化、大笔支出或欠债? 钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗? 仍问财务经历+时间
你长期做什么工作? 你平时主要做什么工作? 仍是 occupation collect
家里有没有结婚、添丁或住院这类记得住时间的事?… 家里如果有结婚、添丁或住院这类事,记得大概哪年就行。 仍是 family collect
可以再说一件记得大概时间的经历。 也可以再说一件你记得大概时间的事。 仍是 other collect
请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。 直接点下面的选项就行,打字回答也一样算数。 点选与打字仍等价计入当前 focus
我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 仍是未对准当前题,不改槽
当前问题已更新,请刷新后重新作答。 这一问刚换成新的,刷新后再答就行。 仍是 409/stale 刷新
当前可信区间是 X–Y,代表分钟 Z。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 交付轮带进度 +「这只是代表性候选,不是已确认的唯一出生分钟。」中间轮可省略完整边界句 交付/采用必须含唯一分钟边界语义
可以从下面的时间里选一个采用。 可以从下面选一个先用着。 仍是采用,不是已确认
接下来请点选下面这一问。 已删除(死文案) 不得再出现占位句
每轮正文只输出一句确认或承接 每轮正文 2–4 句…不提问、不复述题干、不预告选项 问题槽红线不动

UNVERIFIED_BIRTH_TIME_NOTICE 按任务书跳过。

测试断言若从逐字旧句改为语义匹配,上表即对应清单。机器腔词表(≥8 条,含本次清理的全部旧词)在 agent-voice-copy-contract.test.ts

Test plan

  • cd frontend && ./node_modules/.bin/tsc --noEmit exit 0
  • rectification-* 非数据库测试 fail=0(730)
  • consultation-* / consult-* / chat-* fail=0(289,含 voice contract)
  • agent-voice-copy-contract.test.ts:词表、边界语义、E-1 矩阵、E-2 display copy
  • 未在真实 staging / jyotisha.chat 上跑开场双问或婚姻咨询
  • 数据库 rectification-*-database.test.ts:本机 Docker 下有 3 条 database migration failed(并行时)/ 串行重试后 PR-4 与 agent-api migration 仍失败。本 PR 未改 SQL 或 migration runner;与文案无关。ingest P0、v9 open、evidence lifecycle、legacy backfill、profile gating(重试)已绿。

未验证

没有在线上 staging 用事故 case 走一遍真实模型开场。对照与红线证明来自本机源码和测试。

## Summary Agent 可见输出改成先说人话,再守红线。基线是当时的 `origin/staging` @ `8617eb56`;提交前已并入当前 `origin/staging`(`8b7eda9f`,skill-parity 任务书)。顺序:任务 0 对照 → A → B → C → E → D。 - 咨询:voice 置顶;本命仍是 3–6 句口语开场 + 不变的 Level 2 标题顺序。general / window / onboarding 只拿人设,不套本命骨架。 - 校正:正文 2–4 句承接;硬编码用户文案集中到 `frontend/src/lib/rectification-agentic/user-copy.ts`。 - E-1:opening + collect + 本轮已有非空 assistant 正文时,focus 仍持久化,但不再把题干写成第二条助手消息。判定只看 action / kind / intent / 正文是否非空,不读正文内容。 - E-2:引擎 `user_meaning` 仍给模型/审计;用户可见面走 `engineMeaningToDisplayCopy`。Python 引擎字段未改。skill 仍是 10.0.14。 ## 任务 0 · 事故 case `f83d9b42` 改前 / 改后 形状:Skill 10.0.14;开场窗 04:45–05:15(30 分钟)收到 05:00–05:07(7 分钟);代表分钟 05:00。改前串按 `8617eb56` 硬编码 + route 直出复排,冻在 `ACCIDENT_CASE_BEFORE_COPY`。改后由 `accidentCaseHardcodedTurns` 从 copy 模块重建。 ### 校正 assistant 串 | 槽 | 改前 | 改后 | |---|---|---| | 开场采集(第二条独立消息) | 请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。 | **不再追加第二条消息。** focus 仍是同一句采集题,由问题槽渲染。模型正文自己承接。 | | 区分题转向 | 请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。 | 直接点下面的选项就行,打字回答也一样算数。 | | 未对准当前题 | 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 | 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 | | 题已换 | 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | | 中间轮区间 | 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 | 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。眼下更像 05:00。 | | 穷尽后财务采集 | 有没有记得住时间的收入变化、大笔支出或欠债? | 钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗? | | 交付/采用 | 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 可以从下面的时间里选一个采用。 | 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。代表分钟是 05:00。这只是代表性候选,不是已确认的唯一出生分钟。 可以从下面选一个先用着。 | 对照里开场不再出现语义重复的两连问(E-1)。 ### 婚姻主题咨询开场三段 **改前:** 第一眼就是 `## 统一参数与原始结构`,然后 `## 关系`,文末 `## 技法审计表`。 **改后:** 先 3–6 句无标题口语(方向 + 一句承接),再 `## 统一参数与原始结构` → `## 关系` → `## 技法审计表`(Level 2 顺序不变)。 ## 红线 1 · 真实性 / 边界语义 - 技法审计表仍要求写在本命正文文末(`natalSpokenReportContract` + consult user turn)。 - `answer_policy` / precise-timing 守卫未动。 - 交付/采用轮必须含「不是已确认的唯一出生分钟」语义:`containsBoundarySemantics` + `REPRESENTATIVE_MINUTE_DISCLAIMER`。事故 case 交付句见上表。 - 中间轮允许省略完整边界句(见上表 intermediateRange)。 - 证明:`frontend/tests/agent-voice-copy-contract.test.ts`(delivery/adopt 语义、中间轮不含完整边界句);`rectification-provisional-adopt.test.ts` B2;`rectification-answer-choice.test.ts` 采用叙述。 ## 红线 2 · 问题槽归属 - 服务端 focus 仍是唯一问题源。正文指令仍是「不得提问、复述、改写或拼接题干」。放宽的只是 2–4 句承接。 - 选择卡仍由结构化槽位承载。`GENERIC_COLLECT_QUESTION` 仍由 `spokenFollowupForUser` / focus.prompt 写出,不由模型自拟。 - 证明:`rectification-server-focus.test.ts`、`rectification-decision-authority.test.ts` invariant 3(`answer_choice` 仍把 `p_assistant_message === currentQuestion.prompt` 写入历史)、`agent-voice-copy-contract.test.ts` 的 prompt 源扫描。 ## 任务 E-1 · 结构化判定(不得用正文正则) `shouldPersistFocusPromptTurn` 为 false **仅当**: `action === "opening"` 且 (`kind === "collect_spoken"` 或 `intent === "collect_method_evidence"`) 且 `assistantBodyPresent === true`。 矩阵(测试锁定): | action | kind | intent | body | 追加题干消息 | |---|---|---|---|---| | opening | collect_spoken | collect_method_evidence | 有 | 否 | | opening | collect_spoken | collect_method_evidence | 无 | 是 | | opening | choice | distinguish_candidates | 有 | 是 | | message | collect_spoken | collect_method_evidence | 有 | 是 | | answer_choice | choice | distinguish_candidates | 有 | 是 | 源码证明:`shouldPersistFocusPromptTurn` 函数体没有 `.test(` / `.match(` / `.search(` / `includes(`。stream 路径把 `assistantBodyPresent: Boolean(result.answerText?.trim())` 传入,只问「有没有非空正文」,不问正文说了什么。BUG-467 记在 `docs/BUG_HISTORY.md`(复发自 BUG-460;不得用正文字符串判断「是否已问过」)。 ## 任务 E-2 · user_meaning 分层 用户可见(已改走 display copy): - 盘面 stage / lagna contrast / dasha ledger / dasha agreement / nakshatra(`rectification-board.tsx`) - 确认门 blocker(`ConfirmationGateDisclosure`) - distinguish `event_quality` 选择卡 prompt(`choice-card.ts`) - `prospectiveWindowsNarration` / hostNarration 附加句 仍进模型/审计(原样,含「不得…」): - `sessionOutcomeView`、`buildNextUserAction`、skill verification report、probe briefs、Python 引擎 `user_meaning` 引擎字段未删、未改语义。 ## 文案 旧 → 新 → 保留的语义约束 | 旧 | 新 | 约束 | |---|---|---| | 请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。 | 从你最容易想起来的开始就好——比如哪年上的大学、哪年换的工作,记得大概年份就行。 | 仍是口述采集;不改题干所有权 | | 有没有记得住时间的收入变化、大笔支出或欠债? | 钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗? | 仍问财务经历+时间 | | 你长期做什么工作? | 你平时主要做什么工作? | 仍是 occupation collect | | 家里有没有结婚、添丁或住院这类记得住时间的事?… | 家里如果有结婚、添丁或住院这类事,记得大概哪年就行。 | 仍是 family collect | | 可以再说一件记得大概时间的经历。 | 也可以再说一件你记得大概时间的事。 | 仍是 other collect | | 请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。 | 直接点下面的选项就行,打字回答也一样算数。 | 点选与打字仍等价计入当前 focus | | 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 | 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 | 仍是未对准当前题,不改槽 | | 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 仍是 409/stale 刷新 | | 当前可信区间是 X–Y,代表分钟 Z。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 | 交付轮带进度 +「这只是代表性候选,不是已确认的唯一出生分钟。」中间轮可省略完整边界句 | 交付/采用必须含唯一分钟边界语义 | | 可以从下面的时间里选一个采用。 | 可以从下面选一个先用着。 | 仍是采用,不是已确认 | | 接下来请点选下面这一问。 | 已删除(死文案) | 不得再出现占位句 | | 每轮正文只输出一句确认或承接 | 每轮正文 2–4 句…不提问、不复述题干、不预告选项 | 问题槽红线不动 | `UNVERIFIED_BIRTH_TIME_NOTICE` 按任务书跳过。 测试断言若从逐字旧句改为语义匹配,上表即对应清单。机器腔词表(≥8 条,含本次清理的全部旧词)在 `agent-voice-copy-contract.test.ts`。 ## Test plan - [x] `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0 - [x] `rectification-*` 非数据库测试 fail=0(730) - [x] `consultation-*` / `consult-*` / `chat-*` fail=0(289,含 voice contract) - [x] `agent-voice-copy-contract.test.ts`:词表、边界语义、E-1 矩阵、E-2 display copy - [ ] 未在真实 staging / `jyotisha.chat` 上跑开场双问或婚姻咨询 - 数据库 `rectification-*-database.test.ts`:本机 Docker 下有 3 条 `database migration failed`(并行时)/ 串行重试后 PR-4 与 agent-api migration 仍失败。本 PR 未改 SQL 或 migration runner;与文案无关。ingest P0、v9 open、evidence lifecycle、legacy backfill、profile gating(重试)已绿。 ## 未验证 没有在线上 staging 用事故 case 走一遍真实模型开场。对照与红线证明来自本机源码和测试。
jesse added 2 commits 2026-09-02 02:32:43 +08:00
Natal answers were opening on parameter tables, and rectification turns were one-sentence legal copy. Centralize user-facing strings, keep representative-minute and question-slot red lines, and stop duplicating the opening collect prompt as a second assistant message.

Co-authored-by: Cursor <cursoragent@cursor.com>
merge origin/staging into agent-voice branch
Independent Staging Quality Gate / validate (pull_request) Successful in 18m22s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
20a04dcaf2
Some checks are pending
Independent Staging Quality Gate / validate (pull_request) Successful in 18m22s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin codex/agent-voice-20260901:codex/agent-voice-20260901
git checkout codex/agent-voice-20260901
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: root/Jyotisha#48