fix(rectification): hang spoken year follow-ups and drop engine representative from model tables (BUG-678/676)
Independent Staging Quality Gate / validate (push) Failing after 11m54s
Independent Staging Quality Gate / publish (push) Skipped

Spoken collect without askedTurnId now hangs on the last assistant message, remaining-candidate copy uses credible_range, and engine representativeTime no longer keeps eliminated minutes in model house tables.
This commit is contained in:
jesse-ux
2026-09-14 12:12:27 +08:00
parent a5acd535d5
commit 2dbcf2661b
16 changed files with 377 additions and 29 deletions
@@ -332,6 +332,8 @@ function adoptHostNarration(input: {
input.dossier.evidence,
input.dossier.conversationSummary.declinedSkippedTopics,
catalog.remainingSplitTimes,
undefined,
catalog.remainingCredibleRange,
);
if (!hint || delivered.includes(hint)) return delivered;
return `${delivered} ${hint}`.replace(/\s+/g, " ").trim();
@@ -1781,6 +1783,7 @@ async function persistExhaustionCollect(input: {
dossier.conversationSummary.declinedSkippedTopics,
catalog.remainingSplitTimes,
catalog.remainingCandidateCount,
catalog.remainingCredibleRange,
);
const trainingGate = trainingScoreableGate(dossier.evidence);
const ceiling = engineCapabilityCeilingFromReceipt(receipt);
@@ -1889,6 +1892,7 @@ async function persistExhaustionCollect(input: {
dossier.conversationSummary.declinedSkippedTopics,
catalog.remainingSplitTimes,
catalog.remainingCandidateCount,
catalog.remainingCredibleRange,
);
targetedSkipCount += 1;
continue;
@@ -1972,6 +1976,7 @@ async function persistExhaustionCollect(input: {
dossier.conversationSummary.declinedSkippedTopics,
catalog.remainingSplitTimes,
catalog.remainingCandidateCount,
catalog.remainingCredibleRange,
);
const range = nonConvergingRangeNarration({
credibleRange: decision.credibleRange ?? input.decision.credibleRange,
@@ -13,7 +13,7 @@ export function slimDecisionReceipt(
: dossier.latestResult?.candidates ?? []) {
activeTimes.add(candidate.time);
}
for (const value of [inference?.representative_time, dossier.latestResult?.representativeTime, dossier.latestResult?.selectedTime]) {
for (const value of [inference?.representative_time, dossier.latestResult?.selectedTime]) {
if (typeof value === "string" && value.trim()) activeTimes.add(value.slice(0, 5));
}
const focusSchema = dossier.conversationSummary.activeFocus?.expectedAnswerSchema ?? {};
@@ -751,13 +751,23 @@ function remainingTargetedDomains(
return domains;
}
function clockRangePair(
value: readonly [string, string] | null | undefined,
): readonly [string, string] | null {
const start = clockValue(value?.[0]);
const end = clockValue(value?.[1]);
return start && end ? [start, end] : null;
}
export function remainingCandidatesLine(
splitTimes: readonly [string, string] | null | undefined,
candidateCount: number,
examples: readonly string[],
credibleRange?: readonly [string, string] | null,
): string | null {
if (!splitTimes?.[0] || !splitTimes[1] || candidateCount < 1 || examples.length === 0) return null;
return `现在还剩 ${splitTimes[0]}${splitTimes[1]}${candidateCount} 个候选,能把它们分开的是这几条线:${examples.join("、")}`;
const range = clockRangePair(credibleRange) ?? clockRangePair(splitTimes);
if (!range || candidateCount < 1 || examples.length === 0) return null;
return `现在还剩 ${range[0]}${range[1]}${candidateCount} 个候选,能把它们分开的是这几条线:${examples.join("、")}`;
}
export function targetedCollectPool(
@@ -766,6 +776,7 @@ export function targetedCollectPool(
declinedTopics: readonly CollectionTopic[] = [],
splitTimes?: readonly [string, string] | null,
candidateCount?: number,
credibleRange?: readonly [string, string] | null,
): CollectionPoolItem[] {
if (pendingTargetedYearDomain(declinedTopics, evidence)) return [];
const declined = collectDeclinedKinds(declinedTopics);
@@ -773,7 +784,7 @@ export function targetedCollectPool(
if (domains.length === 0) return [];
const count = candidateCount ?? 0;
const examples = domains.map((domain) => TARGETED_EXAMPLES[domain][0]);
const remainingLine = remainingCandidatesLine(splitTimes, count, examples);
const remainingLine = remainingCandidatesLine(splitTimes, count, examples, credibleRange);
return domains.map((domain) => {
const existencePrompt = TARGETED_EXISTENCE_PROMPT[domain];
return {
@@ -821,15 +832,24 @@ export function rangeNarrowHint(
declinedTopics: readonly CollectionTopic[] = [],
splitTimes?: readonly [string, string] | null,
candidateCount?: number,
credibleRange?: readonly [string, string] | null,
): string {
const open = targetedCollectPool(remainingLayers, evidence, [], splitTimes, candidateCount);
const open = targetedCollectPool(remainingLayers, evidence, [], splitTimes, candidateCount, credibleRange);
const remaining = remainingCandidatesLine(
splitTimes,
candidateCount ?? 0,
open.map((item) => TARGETED_EXAMPLES[item.domain as CollectKind]?.[0] ?? item.examples?.[0] ?? "").filter(Boolean),
credibleRange,
);
const hint = targetedCollectHintFromPool(open)
?? targetedCollectHint(targetedCollectPool(remainingLayers, evidence, declinedTopics, splitTimes, candidateCount)[0])
?? targetedCollectHint(targetedCollectPool(
remainingLayers,
evidence,
declinedTopics,
splitTimes,
candidateCount,
credibleRange,
)[0])
?? `还能再收窄:${moreCollectHint(evidence, declinedTopics)}`;
if (remaining && !hint.startsWith(remaining)) {
return `${remaining}${hint}`;
@@ -393,6 +393,7 @@ export function rectificationFollowupCatalog(
remainingLayers,
remainingSplitTimes: remainingSplitTimes(activeTimes.length ? activeTimes : topCandidateTimes),
remainingCandidateCount: remainingCandidateCount(activeTimes.length ? activeTimes : topCandidateTimes),
remainingCredibleRange: inference?.credible_range ?? null,
};
}
@@ -779,6 +780,7 @@ export function decideFromDossier(
remainingLayers: catalog.remainingLayers,
remainingSplitTimes: catalog.remainingSplitTimes,
remainingCandidateCount: catalog.remainingCandidateCount,
remainingCredibleRange: catalog.remainingCredibleRange,
...followupCaseArgs({
stage: dossier.case.stage,
blockScan: dossier.case.blockScan,
@@ -898,6 +900,7 @@ export function decideAfterInferenceChange(input: {
remainingLayers: catalog.remainingLayers,
remainingSplitTimes: catalog.remainingSplitTimes,
remainingCandidateCount: catalog.remainingCandidateCount,
remainingCredibleRange: catalog.remainingCredibleRange,
...followupCaseArgs({
stage: input.dossier.case.stage,
blockScan: input.dossier.case.blockScan,
@@ -416,6 +416,7 @@ export function buildRangeDelivery(input: {
input.declinedTopics ?? [],
remainingSplitTimes(activeTimes),
remainingCandidateCount(activeTimes),
inference?.credible_range,
),
tie_break_available: input.tieBreakAvailable === true,
};
@@ -1448,6 +1448,7 @@ export function targetedCollectFollowup(
declinedTopics: readonly Readonly<Record<string, unknown>>[] = [],
splitTimes?: readonly [string, string] | null,
candidateCount?: number,
credibleRange?: readonly [string, string] | null,
): MethodFollowup | null {
const pendingYear = pendingTargetedYearDomain(declinedTopics, evidence);
if (pendingYear) {
@@ -1459,9 +1460,10 @@ export function targetedCollectFollowup(
declinedTopics,
splitTimes,
candidateCount,
credibleRange,
)[0];
if (!top) return null;
return targetedCollectExistenceFollowup(top, splitTimes, candidateCount);
return targetedCollectExistenceFollowup(top, splitTimes, candidateCount, credibleRange);
}
export function targetedCollectYearFollowup(domain: CollectKind): MethodFollowup {
@@ -1490,6 +1492,7 @@ function targetedCollectExistenceFollowup(
item: CollectionPoolItem,
splitTimes?: readonly [string, string] | null,
candidateCount?: number,
credibleRange?: readonly [string, string] | null,
): MethodFollowup {
const base = followupFromPoolItem(item);
const frame = buildTargetedCollectExistenceFrame({
@@ -1502,6 +1505,7 @@ function targetedCollectExistenceFollowup(
splitTimes,
candidateCount ?? remainingCandidateCount(),
item.examples?.length ? [item.examples[0]!] : [],
credibleRange,
);
return {
...base,
@@ -2098,6 +2102,7 @@ export function buildMethodFollowupPlan(input: {
remainingLayers?: readonly string[];
remainingSplitTimes?: readonly [string, string] | null;
remainingCandidateCount?: number;
remainingCredibleRange?: readonly [string, string] | null;
tieBreakRequested?: boolean;
}): MethodFollowupPlan {
const makeFollowup = (
@@ -2762,6 +2767,7 @@ export function buildMethodFollowupPlan(input: {
],
input.remainingSplitTimes,
input.remainingCandidateCount,
input.remainingCredibleRange,
))
) {
// BUG-651: no yearless personality as the next discriminator.
@@ -2963,6 +2969,7 @@ export function buildMethodFollowupPlan(input: {
],
input.remainingSplitTimes,
input.remainingCandidateCount,
input.remainingCredibleRange,
);
if (targeted) next = targeted;
else if (pendingHoldout) next = makeFollowup(pendingHoldout, false);
@@ -25,7 +25,8 @@ const ANSWER_OPTIONS = new Set(["A", "B", "C", "D", "stop", "skip_probe"]);
export function focusSpokenPrompt(schema: Readonly<Record<string, unknown>> | null | undefined): string | null {
if (!schema) return null;
const top = typeof schema.prompt === "string" ? schema.prompt.trim() : "";
if (top.length >= 8 && top.length <= 120) return top;
// Collect schemas keep short stems such as "大概哪年几月?" (7 chars).
if (top && top.length <= 120 && (schema.collect === true || top.length >= 8)) return top;
const copy = parseAgentChoiceCopy(schema);
const fromChoice = copy?.prompt?.trim() ?? "";
return fromChoice || null;
@@ -107,9 +108,7 @@ export function attachQuestionsToTurns<T extends { id: string; role: string; tex
const hanging: ConversationFocus[] = [];
for (const focus of focuses) {
if (!focus.askedTurnId) {
if (focus.status === "active" && turnQuestionKind(focus) === "choice") {
hanging.push(focus);
}
if (focus.status === "active") hanging.push(focus);
continue;
}
byTurn.set(focus.askedTurnId, focus);