fix(consult): a stored smalltalk reply is not a prior answer; summary counts as one (acceptance fix for BUG-1071)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
2b435f11d0
commit
9d01757f27
+2
-2
@@ -14395,8 +14395,8 @@
|
|||||||
- 现象:会话里第二个问题起(「那我爸呢」「我妈是不是不太在意我」),回答仍是口语开场加四个二级标题的完整解读,重述上一轮已经写过的宫位;用户感受是「太宽泛」。
|
- 现象:会话里第二个问题起(「那我爸呢」「我妈是不是不太在意我」),回答仍是口语开场加四个二级标题的完整解读,重述上一轮已经写过的宫位;用户感受是「太宽泛」。
|
||||||
- 触发条件:同一会话已有一轮本命解读后的任何追问。
|
- 触发条件:同一会话已有一轮本命解读后的任何追问。
|
||||||
- 根因:`jyotishInstructions`「Call run-jyotish-consultation before answering every turn, including short follow-ups」、`natalSpokenReportContract` 末句「One natal … question still uses the opener-plus-skeleton above」、每轮用户轮的 `natalAnswerShapeInstruction()`「一次写完这四个二级标题」三处都不区分首轮与追问轮。
|
- 根因:`jyotishInstructions`「Call run-jyotish-consultation before answering every turn, including short follow-ups」、`natalSpokenReportContract` 末句「One natal … question still uses the opener-plus-skeleton above」、每轮用户轮的 `natalAnswerShapeInstruction()`「一次写完这四个二级标题」三处都不区分首轮与追问轮。
|
||||||
- 修复:`consultation-thinking-plan.ts` 新增 `followUpAnswerShapeInstruction()`(≤ 200 字、第一句答问的那句、是非题先答是 / 不是、最多三处盘面事实、不写二级标题、不写行动清单、不重述上一轮、不解释用户为什么问;明确换领域或要求完整看才回首轮形状)、`hasPriorAssistantAnswer(history)`(存量历史里已有非空 assistant 正文即追问轮,不看问句字面)与 `natalUserTurnShape()`(daily 不变 / 追问 / 首轮);`route.ts` 用户轮按它切指令,grounded 兜底同样切换(同一份 `legacyNatalInstruction`);思考栏与工具上下文带 `followUpTurn`。合同末句改为「首轮用开场 + 骨架,追问轮按用户轮里的指令」。工具仍每轮调用,扣点与结算不变(D7)。
|
- 修复:`consultation-thinking-plan.ts` 新增 `followUpAnswerShapeInstruction()`(≤ 200 字、第一句答问的那句、是非题先答是 / 不是、最多三处盘面事实、不写二级标题、不写行动清单、不重述上一轮、不解释用户为什么问;明确换领域或要求完整看才回首轮形状)、`hasPriorAssistantAnswer(history, { summaryText })`(存量历史里已有非空、且不是寒暄回复(`responseKind: "smalltalk"`)的 assistant 正文即追问轮,会话摘要存在也算;不看问句字面。Claude 验收补修:子代理首版只看 role,「你好」的寒暄回复也存成 assistant 消息,会把其后第一个正式问题当追问;历史窗口现在把 `responseKind` 透传到 tail)与 `natalUserTurnShape()`(daily 不变 / 追问 / 首轮);`route.ts` 用户轮按它切指令,grounded 兜底同样切换(同一份 `legacyNatalInstruction`);思考栏与工具上下文带 `followUpTurn`。合同末句改为「首轮用开场 + 骨架,追问轮按用户轮里的指令」。工具仍每轮调用,扣点与结算不变(D7)。
|
||||||
- 验证:新文件 `frontend/tests/consult-answer-the-question-20260927.test.ts`:真实 Agent + 记录提示词的假模型 + 公开盘 golden,追问轮 → 写作步的提示词含「这是追问轮 / 不超过 200 字」、不含三标题指令,工具跑 1 次、模型 2 次,无标题短正文(< 160 字)整段放出并结算扣点 1 次;首轮对照用例保持开场 + 三标题。`consultation-thinking-plan.test.ts` 覆盖 `hasPriorAssistantAnswer` / `natalUserTurnShape` 四种情况(空历史、只有用户轮、空白 assistant、有回答;daily 不受历史影响)。
|
- 验证:新文件 `frontend/tests/consult-answer-the-question-20260927.test.ts`:真实 Agent + 记录提示词的假模型 + 公开盘 golden,追问轮 → 写作步的提示词含「这是追问轮 / 不超过 200 字」、不含三标题指令,工具跑 1 次、模型 2 次,无标题短正文(< 160 字)整段放出并结算扣点 1 次;首轮对照用例保持开场 + 三标题。`consultation-thinking-plan.test.ts` 覆盖 `hasPriorAssistantAnswer` / `natalUserTurnShape` 四种情况(空历史、只有用户轮、空白 assistant、有回答;daily 不受历史影响)。验收补修加:寒暄回复后仍是首轮形状、摘要存在即追问、空白摘要不算;`consultation-session-history.test.ts` 加 `responseKind` 透传用例。
|
||||||
- 防复发:D6 硬红线——追问判定只用历史事实,禁正则识别意图(沿用 BUG-976/977);route 合同测试断言 `natalUserTurnShape({ entrypoint: consultEntrypoint, history })` 与 `followUpTurn` 的接线。
|
- 防复发:D6 硬红线——追问判定只用历史事实,禁正则识别意图(沿用 BUG-976/977);route 合同测试断言 `natalUserTurnShape({ entrypoint: consultEntrypoint, history })` 与 `followUpTurn` 的接线。
|
||||||
- 相关记录:BUG-1070、BUG-1073、BUG-1053(答案由看过工具结果的步写)、BUG-976/977(禁正则)。
|
- 相关记录:BUG-1070、BUG-1073、BUG-1053(答案由看过工具结果的步写)、BUG-976/977(禁正则)。
|
||||||
- 复发自:无
|
- 复发自:无
|
||||||
|
|||||||
@@ -74,3 +74,22 @@
|
|||||||
- `next build` 默认 Turbopack 对软链的 `node_modules` 报 `Symlink … points out of the filesystem root`(与 `PROGRESS-consult-gender-optional-20260927` 同一现象),改用 `next build --webpack` 量 `/` 状态与首屏 gzip;数字口径与该记录一致(rootMainFiles,gzip level 9)。
|
- `next build` 默认 Turbopack 对软链的 `node_modules` 报 `Symlink … points out of the filesystem root`(与 `PROGRESS-consult-gender-optional-20260927` 同一现象),改用 `next build --webpack` 量 `/` 状态与首屏 gzip;数字口径与该记录一致(rootMainFiles,gzip level 9)。
|
||||||
- lint 有 127 条既有 warning(0 error),不在本单范围,未动。
|
- lint 有 127 条既有 warning(0 error),不在本单范围,未动。
|
||||||
- 真实模型输出未验(无凭据):见 `docs/testing/consult-answer-the-question-20260927.md`。
|
- 真实模型输出未验(无凭据):见 `docs/testing/consult-answer-the-question-20260927.md`。
|
||||||
|
|
||||||
|
## Claude 验收补修(2026-09-27 晚)
|
||||||
|
|
||||||
|
- 发现:`hasPriorAssistantAnswer` 只看 `role === "assistant"`,而寒暄快路(BUG-976/977)的回复也以 `responseKind: "smalltalk"` 存成 assistant 消息(`route.ts` `p_response_message`),历史窗口又把 `responseKind` 丢掉。后果:用户先说「你好」再问第一个正式问题,会被当成追问轮(≤ 200 字、无骨架)。会话摘要把早先轮次折掉后 tail 里可能没有 assistant,则反过来误判为首轮。
|
||||||
|
- 修:`consultation-session-history.ts` 的 `StoredTurn` / `ConsultationHistoryMessage` 带可选 `responseKind`,窗口 tail 透传;`hasPriorAssistantAnswer(history, { summaryText })` 忽略寒暄回复、摘要存在即追问;`natalUserTurnShape` 与 `route.ts` 两处调用带 `historyWindow.summaryText`。`product-voice.ts` Never 段一句改措辞(原句第二个分句读成也被禁止)。
|
||||||
|
- 断言三栏:`consult-single-pass-answer-20260927.test.ts`(route 源码匹配:原值不带 summaryText / 新值带 / 原因如上)、`consultation-voice-contract.test.ts`(Never 句:原值分号并列 / 新值拆两句 / 原因措辞)。新增用例见 `consultation-thinking-plan.test.ts`、`consultation-session-history.test.ts`。
|
||||||
|
- 复跑门禁数字见下方「Claude 验收」。
|
||||||
|
|
||||||
|
## Claude 验收(2026-09-27 晚,Node 22.14,本工作树,含补修)
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| `tsc --noEmit` | 0 错 |
|
||||||
|
| `npm run lint` | 0 error(既有 warning 未动) |
|
||||||
|
| `npm test` | 4197(基线 4192,+5);pass 4143 / fail 24 / skipped 30;24 条失败按名称与基线逐条一致(无 Docker 的 DB / 部署套件) |
|
||||||
|
| `next build --webpack` | exit 0;`/`、`/chart`、`/ephemeris`、`/people` 仍 ○ Static;首屏 gzip 以执行方实测 130,933 B(=基线)为准,补修只改服务端与 lib 类型 |
|
||||||
|
| 真机 | 未做(无登录态、无模型凭据),见 `docs/testing/consult-answer-the-question-20260927.md` 8 条 |
|
||||||
|
|
||||||
|
结论:T1–T6 通过;补修一处(寒暄回复误判追问)已并入本分支。推 staging 后核对 `/api/health` 的 `deployment.gitCommit`。
|
||||||
|
|||||||
@@ -13,5 +13,6 @@
|
|||||||
| 5 | 第 1~4 步过程中看步骤栏 | 第一节标题是「先回答你问的这件事」;首轮它的步骤是「用盘上的反差回答这个问题」,第 2、3 步是「直接回答这句话」;没有「先抓住你真正在问的事」;写作行不出现「正在写先回答你的问题」 | |
|
| 5 | 第 1~4 步过程中看步骤栏 | 第一节标题是「先回答你问的这件事」;首轮它的步骤是「用盘上的反差回答这个问题」,第 2、3 步是「直接回答这句话」;没有「先抓住你真正在问的事」;写作行不出现「正在写先回答你的问题」 | |
|
||||||
| 6 | 第 2 步的回答完成后,看用量 / 后台 | 仍然计一次排盘、扣一次点(追问轮照常调用排盘工具) | |
|
| 6 | 第 2 步的回答完成后,看用量 / 后台 | 仍然计一次排盘、扣一次点(追问轮照常调用排盘工具) | |
|
||||||
| 7 | 换无出生分钟的星盘问「今天适合做什么」 | 开场结尾仍有一到三条短行动(无骨架模式行动留在开场);不出现本命结构 | |
|
| 7 | 换无出生分钟的星盘问「今天适合做什么」 | 开场结尾仍有一到三条短行动(无骨架模式行动留在开场);不出现本命结构 | |
|
||||||
|
| 8 | 新会话先发「你好」,收到一句寒暄回复后再问「我和父母的关系怎么看」 | 仍是第 1 步的首轮形状(口语开场 + 三个二级标题),不是 200 字追问形状(寒暄回复不算上一轮解读) | |
|
||||||
|
|
||||||
任一条不符,按 BUG-1070~1073 追加记录,附问句句式与回答的**结构描述**(标题、字数、第一句是不是是 / 不是),不贴回答原文。
|
任一条不符,按 BUG-1070~1073 追加记录,附问句句式与回答的**结构描述**(标题、字数、第一句是不是是 / 不是),不贴回答原文。
|
||||||
|
|||||||
@@ -995,8 +995,8 @@ export async function POST(request: Request) {
|
|||||||
// A session that already holds an answer gets the follow-up shape
|
// A session that already holds an answer gets the follow-up shape
|
||||||
// (BUG-1071): a direct answer, no skeleton. That is read from the stored
|
// (BUG-1071): a direct answer, no skeleton. That is read from the stored
|
||||||
// history, never from the wording of the question.
|
// history, never from the wording of the question.
|
||||||
const followUpTurn = hasPriorAssistantAnswer(history);
|
const followUpTurn = hasPriorAssistantAnswer(history, { summaryText: historyWindow.summaryText });
|
||||||
const natalInstruction = `${natalToolInstruction}${natalUserTurnShape({ entrypoint: consultEntrypoint, history })}`;
|
const natalInstruction = `${natalToolInstruction}${natalUserTurnShape({ entrypoint: consultEntrypoint, history, summaryText: historyWindow.summaryText })}`;
|
||||||
const adoptedRangeNote = consultationMode === "verified_chart"
|
const adoptedRangeNote = consultationMode === "verified_chart"
|
||||||
&& prepared.serverChart?.truth.birthTimeStatus === "accepted"
|
&& prepared.serverChart?.truth.birthTimeStatus === "accepted"
|
||||||
&& prepared.serverChart.toolInput.candidate_range
|
&& prepared.serverChart.toolInput.candidate_range
|
||||||
@@ -1518,7 +1518,7 @@ export async function POST(request: Request) {
|
|||||||
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
|
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
|
||||||
// The grounded fallback writes from the injected workflow; same first-turn
|
// The grounded fallback writes from the injected workflow; same first-turn
|
||||||
// / follow-up split as the agentic path (BUG-1071), one statement of it.
|
// / follow-up split as the agentic path (BUG-1071), one statement of it.
|
||||||
const legacyNatalInstruction = hasPriorAssistantAnswer(history)
|
const legacyNatalInstruction = hasPriorAssistantAnswer(history, { summaryText: historyWindow.summaryText })
|
||||||
? followUpAnswerShapeInstruction()
|
? followUpAnswerShapeInstruction()
|
||||||
: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」;开场不写行动。然后按三个二级标题写:## 盘里支持这个判断的地方(原始结构、六步宫位、Yoga 表作为依据,只写开场没说过的,表格与正文二选一)、## 时间怎么看、## 这周可以做的一件事(最多三条行动,只在这里出现一次)。全文不超过 900 字。骨架不可省略;不写「先回答你的问题」这个标题,文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。";
|
: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」;开场不写行动。然后按三个二级标题写:## 盘里支持这个判断的地方(原始结构、六步宫位、Yoga 表作为依据,只写开场没说过的,表格与正文二选一)、## 时间怎么看、## 这周可以做的一件事(最多三条行动,只在这里出现一次)。全文不超过 900 字。骨架不可省略;不写「先回答你的问题」这个标题,文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。";
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const SESSION_CONTEXT_SUMMARY_HEADING = "【会话摘要(服务端维
|
|||||||
export type ConsultationHistoryMessage = Readonly<{
|
export type ConsultationHistoryMessage = Readonly<{
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant";
|
||||||
text: string;
|
text: string;
|
||||||
|
/** A stored smalltalk reply (BUG-976/977 fast path); it is not a natal answer. */
|
||||||
|
responseKind?: "smalltalk";
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type SessionContextSummaryV1 = Readonly<{
|
export type SessionContextSummaryV1 = Readonly<{
|
||||||
@@ -37,6 +39,7 @@ type StoredTurn = Readonly<{
|
|||||||
role: "user" | "assistant";
|
role: "user" | "assistant";
|
||||||
text: string;
|
text: string;
|
||||||
requestId: string | null;
|
requestId: string | null;
|
||||||
|
responseKind?: "smalltalk";
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
@@ -107,7 +110,7 @@ export function storedConsultationTurns(
|
|||||||
const rows: StoredTurn[] = [];
|
const rows: StoredTurn[] = [];
|
||||||
messages.forEach((message, index) => {
|
messages.forEach((message, index) => {
|
||||||
if (!message || typeof message !== "object") return;
|
if (!message || typeof message !== "object") return;
|
||||||
const stored = message as { role?: unknown; text?: unknown; requestId?: unknown };
|
const stored = message as { role?: unknown; text?: unknown; requestId?: unknown; responseKind?: unknown };
|
||||||
if (options.excludeRequestId && stored.requestId === options.excludeRequestId) return;
|
if (options.excludeRequestId && stored.requestId === options.excludeRequestId) return;
|
||||||
if (stored.role !== "user" && stored.role !== "assistant") return;
|
if (stored.role !== "user" && stored.role !== "assistant") return;
|
||||||
if (typeof stored.text !== "string" || !stored.text) return;
|
if (typeof stored.text !== "string" || !stored.text) return;
|
||||||
@@ -116,6 +119,7 @@ export function storedConsultationTurns(
|
|||||||
role: stored.role,
|
role: stored.role,
|
||||||
text: stored.text,
|
text: stored.text,
|
||||||
requestId: typeof stored.requestId === "string" ? stored.requestId : null,
|
requestId: typeof stored.requestId === "string" ? stored.requestId : null,
|
||||||
|
...(stored.responseKind === "smalltalk" ? { responseKind: "smalltalk" as const } : {}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return rows;
|
return rows;
|
||||||
@@ -156,6 +160,7 @@ export function consultationHistoryWindow(
|
|||||||
const clipped = selected.map((turn) => ({
|
const clipped = selected.map((turn) => ({
|
||||||
role: turn.role,
|
role: turn.role,
|
||||||
text: clipConsultationHistoryText(turn.text),
|
text: clipConsultationHistoryText(turn.text),
|
||||||
|
...(turn.responseKind ? { responseKind: turn.responseKind } : {}),
|
||||||
}));
|
}));
|
||||||
const budget = historyBudgetChars(options.contextWindow);
|
const budget = historyBudgetChars(options.contextWindow);
|
||||||
let droppedCount = 0;
|
let droppedCount = 0;
|
||||||
|
|||||||
@@ -383,11 +383,21 @@ export function followUpAnswerShapeInstruction(): string {
|
|||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The stored history already holds an assistant answer: this turn is a follow-up (D6, no intent regex). */
|
/**
|
||||||
|
* The session already holds a natal answer, so this turn is a follow-up (D6,
|
||||||
|
* no intent regex). A stored smalltalk reply (「你好」 fast path) is an
|
||||||
|
* assistant message but not an answer, so it does not count; a session
|
||||||
|
* context summary only exists after earlier consultation turns were
|
||||||
|
* summarized away, so it counts even when the tail no longer holds them.
|
||||||
|
*/
|
||||||
export function hasPriorAssistantAnswer(
|
export function hasPriorAssistantAnswer(
|
||||||
history: ReadonlyArray<Readonly<{ role: "user" | "assistant"; text: string }>>,
|
history: ReadonlyArray<Readonly<{ role: "user" | "assistant"; text: string; responseKind?: "smalltalk" }>>,
|
||||||
|
options: Readonly<{ summaryText?: string | null }> = {},
|
||||||
): boolean {
|
): boolean {
|
||||||
return history.some((message) => message.role === "assistant" && message.text.trim().length > 0);
|
if (options.summaryText?.trim()) return true;
|
||||||
|
return history.some((message) => (
|
||||||
|
message.role === "assistant" && message.responseKind !== "smalltalk" && message.text.trim().length > 0
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -397,10 +407,11 @@ export function hasPriorAssistantAnswer(
|
|||||||
*/
|
*/
|
||||||
export function natalUserTurnShape(input: {
|
export function natalUserTurnShape(input: {
|
||||||
entrypoint?: string;
|
entrypoint?: string;
|
||||||
history: ReadonlyArray<Readonly<{ role: "user" | "assistant"; text: string }>>;
|
history: ReadonlyArray<Readonly<{ role: "user" | "assistant"; text: string; responseKind?: "smalltalk" }>>;
|
||||||
|
summaryText?: string | null;
|
||||||
}): string {
|
}): string {
|
||||||
if (input.entrypoint === "daily_starlanguage") return dailyAnswerShapeInstruction();
|
if (input.entrypoint === "daily_starlanguage") return dailyAnswerShapeInstruction();
|
||||||
return hasPriorAssistantAnswer(input.history)
|
return hasPriorAssistantAnswer(input.history, { summaryText: input.summaryText })
|
||||||
? followUpAnswerShapeInstruction()
|
? followUpAnswerShapeInstruction()
|
||||||
: natalAnswerShapeInstruction();
|
: natalAnswerShapeInstruction();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,6 @@ Raman yogas and house steps remain governed_support_only_candidate_ready: write
|
|||||||
Never:
|
Never:
|
||||||
- Skip a card fact the question needs in order to keep the answer short — and never write a card fact the question does not need in order to look thorough.
|
- Skip a card fact the question needs in order to keep the answer short — and never write a card fact the question does not need in order to look thorough.
|
||||||
- Put the Technique Audit Table or 统一参数 heading back into the spoken body.
|
- Put the Technique Audit Table or 统一参数 heading back into the spoken body.
|
||||||
- State what the user or the person asked about wants, needs, or feels; state where the chart puts them.
|
- State what the user or the person asked about wants, needs, or feels. Say where the chart puts them instead.
|
||||||
|
|
||||||
The first natal career/wealth/marriage/family answer of a session uses the opener-plus-skeleton above; a follow-up turn follows the shape instruction carried in the user turn (a direct answer of about 200 characters, no headings) unless the user changes domain or asks for the full reading.`;
|
The first natal career/wealth/marriage/family answer of a session uses the opener-plus-skeleton above; a follow-up turn follows the shape instruction carried in the user turn (a direct answer of about 200 characters, no headings) unless the user changes domain or asks for the full reading.`;
|
||||||
|
|||||||
@@ -432,8 +432,11 @@ test("the consult route has no separate compose stream and wires the single-pass
|
|||||||
// 原值: assert.match(route, /natalAnswerShapeInstruction\(\)/)
|
// 原值: assert.match(route, /natalAnswerShapeInstruction\(\)/)
|
||||||
// 新值: 用户轮形状经 natalUserTurnShape 按「会话已有回答」切换首轮 / 追问轮,思考栏与工具上下文带 followUpTurn
|
// 新值: 用户轮形状经 natalUserTurnShape 按「会话已有回答」切换首轮 / 追问轮,思考栏与工具上下文带 followUpTurn
|
||||||
// 原因: TASK-consult-answer-the-question-20260927 D3/D6(BUG-1071)
|
// 原因: TASK-consult-answer-the-question-20260927 D3/D6(BUG-1071)
|
||||||
assert.match(route, /natalUserTurnShape\(\{ entrypoint: consultEntrypoint, history \}\)/);
|
// 原值: natalUserTurnShape({ entrypoint, history }) / hasPriorAssistantAnswer(history)
|
||||||
assert.match(route, /const followUpTurn = hasPriorAssistantAnswer\(history\);/);
|
// 新值: 两处都带 summaryText(Claude 验收补修:寒暄回复不算上一轮解读,摘要算)
|
||||||
|
// 原因: 「你好」的回复也存成 assistant 消息,只看 role 会把第一个正式问题当追问
|
||||||
|
assert.match(route, /natalUserTurnShape\(\{ entrypoint: consultEntrypoint, history, summaryText: historyWindow\.summaryText \}\)/);
|
||||||
|
assert.match(route, /const followUpTurn = hasPriorAssistantAnswer\(history, \{ summaryText: historyWindow\.summaryText \}\);/);
|
||||||
assert.match(route, /followUp: followUpTurn,/);
|
assert.match(route, /followUp: followUpTurn,/);
|
||||||
assert.match(route, /followUpTurn,\n\s+serverChart: prepared\.serverChart,/);
|
assert.match(route, /followUpTurn,\n\s+serverChart: prepared\.serverChart,/);
|
||||||
assert.doesNotMatch(route, /natalAnswerShapeInstruction\(\)/);
|
assert.doesNotMatch(route, /natalAnswerShapeInstruction\(\)/);
|
||||||
|
|||||||
@@ -73,6 +73,19 @@ test("a 20_000 character message is clipped to 12_000 with an omission count", (
|
|||||||
assert.equal(history.tail[0]?.text, clipped);
|
assert.equal(history.tail[0]?.text, clipped);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a stored smalltalk reply keeps its responseKind through the history window", () => {
|
||||||
|
// Claude acceptance fix for the follow-up detection: the route reads responseKind
|
||||||
|
// off the window tail to tell a 「你好」 reply from a natal answer.
|
||||||
|
const history = consultationHistoryWindow([
|
||||||
|
{ role: "user", text: "你好" },
|
||||||
|
{ role: "assistant", text: "你好,想聊什么都可以", responseKind: "smalltalk" },
|
||||||
|
{ role: "user", text: "我和父母的亲疏关系如何" },
|
||||||
|
{ role: "assistant", text: "外顺内远。" },
|
||||||
|
], null, { contextWindow: 128_000 });
|
||||||
|
assert.equal(history.tail[1]?.responseKind, "smalltalk");
|
||||||
|
assert.equal("responseKind" in (history.tail[3] ?? {}), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("historyBudgetChars uses 64k, 32k, and null-as-128k windows", () => {
|
test("historyBudgetChars uses 64k, 32k, and null-as-128k windows", () => {
|
||||||
assert.equal(historyBudgetChars(64_000), 6_000);
|
assert.equal(historyBudgetChars(64_000), 6_000);
|
||||||
assert.equal(historyBudgetChars(32_000), 4_000);
|
assert.equal(historyBudgetChars(32_000), 4_000);
|
||||||
|
|||||||
@@ -232,6 +232,20 @@ test("a follow-up turn answers directly and the thinking bar says so (TASK-consu
|
|||||||
{ role: "user", text: "我和父母的亲疏关系如何" },
|
{ role: "user", text: "我和父母的亲疏关系如何" },
|
||||||
{ role: "assistant", text: "外顺内远。" },
|
{ role: "assistant", text: "外顺内远。" },
|
||||||
]), true);
|
]), true);
|
||||||
|
// Claude acceptance fix: a stored smalltalk reply (「你好」 fast path, responseKind "smalltalk")
|
||||||
|
// is an assistant message but not a natal answer, so the first real question after it
|
||||||
|
// still gets the first-turn shape; a session summary means earlier answers were folded away.
|
||||||
|
assert.equal(hasPriorAssistantAnswer([
|
||||||
|
{ role: "user", text: "你好" },
|
||||||
|
{ role: "assistant", text: "你好,想聊什么都可以", responseKind: "smalltalk" },
|
||||||
|
]), false);
|
||||||
|
assert.equal(hasPriorAssistantAnswer([], { summaryText: "此前一轮讲过父母:外顺内远。" }), true);
|
||||||
|
assert.equal(hasPriorAssistantAnswer([], { summaryText: " " }), false);
|
||||||
|
assert.equal(
|
||||||
|
natalUserTurnShape({ history: [{ role: "user", text: "你好" }, { role: "assistant", text: "你好", responseKind: "smalltalk" }] }),
|
||||||
|
natalAnswerShapeInstruction(),
|
||||||
|
);
|
||||||
|
assert.equal(natalUserTurnShape({ history: [], summaryText: "此前一轮讲过父母。" }), followUpAnswerShapeInstruction());
|
||||||
|
|
||||||
// ① empty history → first-turn shape; ② prior answer → follow-up; ④ daily is untouched by history.
|
// ① empty history → first-turn shape; ② prior answer → follow-up; ④ daily is untouched by history.
|
||||||
const first = natalUserTurnShape({ history: [] });
|
const first = natalUserTurnShape({ history: [] });
|
||||||
|
|||||||
@@ -182,7 +182,9 @@ test("the voice answers the sentence asked: no motive guessing, yes/no first, co
|
|||||||
assert.match(voice, /Good \(是非题,用户问「我妈是不是不太在意我」/);
|
assert.match(voice, /Good \(是非题,用户问「我妈是不是不太在意我」/);
|
||||||
assert.match(voice, /不是。4 宫主水星逆行落 10 宫/);
|
assert.match(voice, /不是。4 宫主水星逆行落 10 宫/);
|
||||||
assert.match(voice, /Bad \(同一问题\): 你其实要的是被听见/);
|
assert.match(voice, /Bad \(同一问题\): 你其实要的是被听见/);
|
||||||
assert.match(voice, /State what the user or the person asked about wants, needs, or feels; state where the chart puts them/);
|
// 原值: "…wants, needs, or feels; state where the chart puts them"(Never 列表里第二句读成也被禁止)
|
||||||
|
// 新值: 两句拆开,第二句是替代做法 原因: Claude 验收改措辞
|
||||||
|
assert.match(voice, /State what the user or the person asked about wants, needs, or feels\. Say where the chart puts them instead/);
|
||||||
// The three-mode shared voice still carries all of it (general and window expand the same literal).
|
// The three-mode shared voice still carries all of it (general and window expand the same literal).
|
||||||
const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract"));
|
const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract"));
|
||||||
assert.ok(sharedVoice.includes("不揣测(回答对象是用户问的那句话)"));
|
assert.ok(sharedVoice.includes("不揣测(回答对象是用户问的那句话)"));
|
||||||
|
|||||||
Reference in New Issue
Block a user