fix(rectification): keep opening greeting across set-focus
Independent Staging Quality Gate / validate (push) Successful in 9m24s
Independent Staging Quality Gate / publish (push) Successful in 1m57s

set-focus only attaches the question stem. Retracting live Chinese before
it wiped the opening hello with an empty replace. Other public tools still
retract planning prose.

BUG-533.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 19:51:24 +08:00
parent be6cf145c2
commit a793edb1ad
4 changed files with 69 additions and 2 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-04 — 开场打招呼不再被 set-focus 清掉
生时校正开场先播出的打招呼会留在同一条消息里。`rectification-set-focus` 只负责题干,不再发空的 `replace` 把正文抹掉。Skill 版本未变。
## 2026-09-04 — 未确认生时的个人报告不再被候选窗读取打死
出生时间尚未 confirmed 时,报告会去读校正候选窗。那张表已经收回运行时表权限,读失败却被当成计算不可用,整份报告在排盘前就失败。现在改走只读 RPC;读不到窗口就按无窗口继续生成。Skill 版本未变。
+16
View File
@@ -8228,3 +8228,19 @@
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-533 | 开场打招呼被 set-focus 空 replace 清掉
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`applyStepAnswerChunk``retractSpoken`、开场轮 `answer.delta`
- 用户现象:开场先流出「你好,很高兴一起做这次生时校正…」,页面最终只剩「我们慢慢来就好…」和采集题干。
- 触发条件:模型在 `rectification-set-focus` 之前写出带句号的中文正文;该步已 `live` 播出。
- 根因:任一公开工具的 `tool-call` 都会 `retractLive`,发出 `answer.delta` `{text:"", replace:true}`。set-focus 只是把题干挂到同一条消息,前面的打招呼不是过程自述。
- 修复:`rectification-set-focus` 的 call/result 不再收回已播出正文。读诊断、记证据仍收回。
- 验证:`rectification-step-answer`:打招呼 + set-focus 为 `none`,其后短句仍 `live`read-diagnostics 仍 `retract`
- 防复发:set-focus 不得把同轮已播出的正文 replace 成空。不得把这个例外扩到 record/compare/diagnostics。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
@@ -6,7 +6,8 @@
* Once the current step looks like the spoken reply — Chinese sentence end,
* or live-unlock after compare/offer/result tools — tokens may stream as
* `live`. A later public tool-call retracts that speculative stream so
* "Let me set" never stays in `answer.delta`.
* "Let me set" never stays in `answer.delta`. Exception: `rectification-set-focus`
* only attaches the question stem; live greeting/handoff in the same step stays.
*/
export type StepAnswerChunk = Readonly<{
@@ -45,6 +46,11 @@ const SPOKEN_LIVE_UNLOCK_TOOLS = new Set([
"rectification-read-diagnostics",
]);
/** set-focus writes the stem onto the same turn; it is not planning prose. */
const KEEP_LIVE_SPOKEN_TOOLS = new Set([
"rectification-set-focus",
]);
function resetBuffers(state: StepAnswerState): void {
state.text = "";
state.pieces = [];
@@ -153,14 +159,19 @@ export function applyStepAnswerChunk(
}
case "tool-call":
if (isPublicToolCall(chunk, isPublicTool)) {
const retracted = retractLive(state);
state.calledTool = true;
if (KEEP_LIVE_SPOKEN_TOOLS.has(toolName(chunk))) return { kind: "none" };
const retracted = retractLive(state);
return retracted.kind === "retract" ? retracted : { kind: "none" };
}
return { kind: "none" };
case "tool-result":
case "tool-error": {
if (isPublicToolCall(chunk, isPublicTool)) state.calledTool = true;
if (KEEP_LIVE_SPOKEN_TOOLS.has(toolName(chunk))) {
resetBuffers(state);
return { kind: "none" };
}
const discarded = state.calledTool || state.text.length > 0;
const retracted = retractLive(state);
resetBuffers(state);
@@ -106,6 +106,42 @@ test("retracts a live spoken prefix if that step later calls a public tool", ()
assert.deepEqual(spoken, { kind: "live", text: "诊断已经看过。接下来确认入职年份。" });
});
test("keeps a live opening greeting when the next public tool is set-focus", () => {
const state = createStepAnswerState();
assert.deepEqual(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "你好,我们从你最容易想起的经历开始就行。" }),
isPublicTool,
),
{ kind: "live", text: "你好,我们从你最容易想起的经历开始就行。" },
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
assert.deepEqual(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "我们慢慢来就好——想到哪件就聊哪件,不用一次说完。" }),
isPublicTool,
),
{ kind: "live", text: "我们慢慢来就好——想到哪件就聊哪件,不用一次说完。" },
);
});
test("does not live-publish English planning before a tool-call", () => {
const state = createStepAnswerState();
assert.equal(