fix(rectification): out-of-window event dates no longer fail every rescore; future dates refused at intake; corrections revise without a focus (BUG-1178, BUG-1179)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e978cabc4
commit
3bbe65a215
@@ -223,6 +223,8 @@ export const RECTIFICATION_USER_COPY = {
|
||||
forceMinuteAfterSubBlocks: "时段分不开,直接按分钟比。",
|
||||
compareFailedRetry: "候选比较这次没跑成,下一句话时会自动再试。",
|
||||
rescoreSkipped: "这次没有重新比较,稍后再比一次。",
|
||||
/** BUG-1178: an event dated after today is not recorded or scored. */
|
||||
futureDatedEvent: "有一件事的日期在今天之后,这件先不算;如果年份打错了,直接告诉我正确的年份就行。",
|
||||
lastSuccessfulCompareRange: "这是按上一次成功比较给出的范围。",
|
||||
deferredCareerWindow: "下一次事业变动的预测窗口留在采用后的核对阶段。",
|
||||
rangeDeliveryTitle: "目前范围",
|
||||
@@ -662,6 +664,7 @@ export function listUserVisibleCopy(): string[] {
|
||||
RECTIFICATION_USER_COPY.postAdoptVerifyDone,
|
||||
RECTIFICATION_USER_COPY.compareFailedRetry,
|
||||
RECTIFICATION_USER_COPY.rescoreSkipped,
|
||||
RECTIFICATION_USER_COPY.futureDatedEvent,
|
||||
RECTIFICATION_USER_COPY.lastSuccessfulCompareRange,
|
||||
RECTIFICATION_USER_COPY.deferredCareerWindow,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryTitle,
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { currentEngineCallTimings, reportTurnProgress } from "./turn-instrumentation.ts";
|
||||
import {
|
||||
batchResultFromToolChunk,
|
||||
batchHasFutureDatedItem,
|
||||
batchRescoreFailed,
|
||||
acceptedRecapLines,
|
||||
composeHostFallbackNarration,
|
||||
@@ -620,6 +621,7 @@ export async function streamV9Attempt(
|
||||
serverFacts = {
|
||||
compareFailed,
|
||||
rescoreSkipped,
|
||||
futureDated: batchHasFutureDatedItem(batchToolResult),
|
||||
rangeBefore: rangeBeforeCompare,
|
||||
rangeAfter: rangeAfterEvidence,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { toAgentModelFinishReason } from "../../agent-observability.ts";
|
||||
import type { PublicStreamEvent } from "./stream-mapping";
|
||||
import type { SpokenFactWhitelist } from "./spoken-grounding";
|
||||
import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
withCompareFailedRetryNotice,
|
||||
withRangeChangedAfterEvidence,
|
||||
withRescoreSkippedNotice,
|
||||
@@ -51,6 +52,8 @@ export type AttemptOutcome = Readonly<{
|
||||
export type TurnServerFacts = Readonly<{
|
||||
compareFailed: boolean;
|
||||
rescoreSkipped: boolean;
|
||||
/** BUG-1178: an item of this turn's batch was dated after today and was not recorded. */
|
||||
futureDated?: boolean;
|
||||
rangeBefore: readonly [string, string] | null;
|
||||
rangeAfter: readonly [string, string] | null;
|
||||
}>;
|
||||
@@ -62,9 +65,11 @@ export function composeSpokenWithServerFacts(
|
||||
): string {
|
||||
let spoken = body.trim();
|
||||
if (!facts) return spoken;
|
||||
const future = facts.futureDated === true ? RECTIFICATION_USER_COPY.futureDatedEvent : "";
|
||||
const withFuture = (text: string) => !future || text.includes(future) ? text : text ? `${text}${/[。!?]$/.test(text) ? "" : "。"}${future}` : future;
|
||||
if (facts.compareFailed) spoken = withCompareFailedRetryNotice(spoken);
|
||||
if (facts.rescoreSkipped) return withRescoreSkippedNotice(spoken);
|
||||
return withRangeChangedAfterEvidence(spoken, facts.rangeBefore, facts.rangeAfter);
|
||||
if (facts.rescoreSkipped) return withFuture(withRescoreSkippedNotice(spoken));
|
||||
return withFuture(withRangeChangedAfterEvidence(spoken, facts.rangeBefore, facts.rangeAfter));
|
||||
}
|
||||
|
||||
export const MAX_ATTEMPTS = 2;
|
||||
|
||||
@@ -769,6 +769,30 @@ export function sanitizeAskedProbeKeysForEngine(
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* BUG-1178: the engine rejects the whole request when any event lies outside
|
||||
* [birth_date, today] (`events[i] dates must be between birth_date and today`). A typo'd future
|
||||
* year, or any 「今年 / 这个月」 event whose period ends after today, then failed every rescore
|
||||
* of the Case and the delivery card never came. Before sending: drop events that start after
|
||||
* the cap or end before birth; clip the rest into the window. The cap is yesterday (UTC) so the
|
||||
* engine's own clock can never be behind ours.
|
||||
*/
|
||||
export function boundEngineEventDates(
|
||||
events: readonly V9EngineEvent[],
|
||||
birthDate: string,
|
||||
now: Date = new Date(),
|
||||
): V9EngineEvent[] {
|
||||
const cap = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10);
|
||||
const floor = /^\d{4}-\d{2}-\d{2}$/.test(birthDate) ? birthDate : null;
|
||||
return events.flatMap((event): V9EngineEvent[] => {
|
||||
if (event.date_start > cap) return [];
|
||||
if (floor && event.date_end < floor) return [];
|
||||
const start = floor && event.date_start < floor ? floor : event.date_start;
|
||||
const end = event.date_end > cap ? cap : event.date_end;
|
||||
return start === event.date_start && end === event.date_end ? [event] : [{ ...event, date_start: start, date_end: end }];
|
||||
});
|
||||
}
|
||||
|
||||
export function engineRequestBody(input: {
|
||||
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
|
||||
candidateRange: DatedCandidateRange;
|
||||
@@ -786,7 +810,8 @@ export function engineRequestBody(input: {
|
||||
if (!birthDate || lat === null || lon === null || tz === null) {
|
||||
throw new RectificationEngineError("engine_profile_incomplete", "server profile snapshot is incomplete");
|
||||
}
|
||||
if (input.events.length === 0) {
|
||||
const events = boundEngineEventDates(input.events, birthDate);
|
||||
if (events.length === 0) {
|
||||
throw new RectificationEngineError("no_scorable_evidence", "no scorable evidence for the engine");
|
||||
}
|
||||
const askedProbeKeys = sanitizeAskedProbeKeysForEngine(input.askedProbeKeys);
|
||||
@@ -805,7 +830,7 @@ export function engineRequestBody(input: {
|
||||
tz,
|
||||
ayanamsa: resolveAyanamsa(snapshot),
|
||||
node_mode: "mean",
|
||||
events: input.events,
|
||||
events,
|
||||
birth_time_source: snapshot.birth_time_source,
|
||||
timezone_id: snapshot.timezone_id,
|
||||
timezone_source: snapshot.timezone_source,
|
||||
|
||||
@@ -119,6 +119,15 @@ export function batchResultFromToolChunk(chunk: {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** BUG-1178: the batch turned an item away because it starts after today. */
|
||||
export function batchHasFutureDatedItem(batchResult: unknown): boolean {
|
||||
if (!batchResult || typeof batchResult !== "object" || Array.isArray(batchResult)) return false;
|
||||
const items = (batchResult as { items?: unknown }).items;
|
||||
return Array.isArray(items) && items.some((item) => (
|
||||
Boolean(item) && typeof item === "object" && (item as { error_code?: unknown }).error_code === "future_date"
|
||||
));
|
||||
}
|
||||
|
||||
export function batchRescoreFailed(batchResult: unknown): boolean {
|
||||
if (!batchResult || typeof batchResult !== "object" || Array.isArray(batchResult)) return false;
|
||||
const rescore = (batchResult as { rescore?: { status?: unknown } }).rescore;
|
||||
|
||||
@@ -254,7 +254,10 @@ export function projectTurnDecision(
|
||||
}
|
||||
return [{ role: turn.role, text }];
|
||||
});
|
||||
const evidence = dossier.evidence.slice(-TURN_DECISION_EVIDENCE_LIMIT).map((item) => ({
|
||||
// BUG-1179: the id lets the agent revise a record the user corrects (「2028 打错了,是 2018」).
|
||||
const evidence = dossier.evidence.filter((item) => item.status !== "superseded")
|
||||
.slice(-TURN_DECISION_EVIDENCE_LIMIT).map((item) => ({
|
||||
evidence_id: item.id,
|
||||
domain: item.domain,
|
||||
kind: item.eventKind,
|
||||
status: item.status,
|
||||
|
||||
Reference in New Issue
Block a user