fix(rectification): persist refreshed probes only when the engine has new questions (BUG-655)
Empty refreshes were writing inference rows, and GET-selected probe keys could miss inference_state, so persist rejected the next card. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { probeFromEngine } from "../core/probes-from-engine.ts";
|
||||
import type { ConflictProbe, InferenceState } from "../core/types.ts";
|
||||
import type { ConflictProbe, InferenceState, ProbeAnswer } from "../core/types.ts";
|
||||
import { askedDiscriminatorKeys, previousInferenceFromReceipt } from "./inference-adapter.ts";
|
||||
import {
|
||||
runV9CandidateScore,
|
||||
@@ -71,21 +71,55 @@ function mergeEventProbes(
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function isRefreshableDatedConflict(probe: ConflictProbe): boolean {
|
||||
if (probe.year <= 0 || probe.choice_kind === "varga_style" || probe.source === "nakshatra_boundary") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function probeIdPatternFromAnswered(answered: readonly ProbeAnswer[]): "simple" | "hashed" {
|
||||
for (const item of answered) {
|
||||
if (!item.probe_id || !item.semantic_key) continue;
|
||||
if (item.probe_id === `probe:${item.semantic_key}` || item.probe_id === item.semantic_key) {
|
||||
return "simple";
|
||||
}
|
||||
if (
|
||||
item.candidate_split_hash
|
||||
&& item.probe_id === `probe:${item.semantic_key}:${item.candidate_split_hash}`
|
||||
) {
|
||||
return "hashed";
|
||||
}
|
||||
}
|
||||
return "simple";
|
||||
}
|
||||
|
||||
export function alignedProbeId(probe: ConflictProbe, answered: readonly ProbeAnswer[]): string {
|
||||
return probeIdPatternFromAnswered(answered) === "hashed"
|
||||
? `probe:${probe.semantic_key}:${probe.candidate_split_hash}`
|
||||
: `probe:${probe.semantic_key}`;
|
||||
}
|
||||
|
||||
function alignedIncomingProbe(probe: ConflictProbe, answered: readonly ProbeAnswer[]): ConflictProbe {
|
||||
const id = alignedProbeId(probe, answered);
|
||||
return probe.id === id ? probe : { ...probe, id };
|
||||
}
|
||||
|
||||
function mergeConflictProbes(
|
||||
existing: readonly ConflictProbe[],
|
||||
incoming: readonly ConflictProbe[],
|
||||
answered: readonly ProbeAnswer[] = [],
|
||||
): ConflictProbe[] {
|
||||
const byKey = new Map<string, ConflictProbe>();
|
||||
for (const probe of existing) {
|
||||
byKey.set(probe.semantic_key, probe);
|
||||
}
|
||||
for (const probe of incoming) {
|
||||
if (probe.year <= 0 || probe.choice_kind === "varga_style" || probe.source === "nakshatra_boundary") {
|
||||
continue;
|
||||
}
|
||||
const current = byKey.get(probe.semantic_key);
|
||||
if (!current || probe.information_gain > current.information_gain) {
|
||||
byKey.set(probe.semantic_key, probe);
|
||||
if (!isRefreshableDatedConflict(probe)) continue;
|
||||
const next = alignedIncomingProbe(probe, answered);
|
||||
const current = byKey.get(next.semantic_key);
|
||||
if (!current || next.information_gain > current.information_gain) {
|
||||
byKey.set(next.semantic_key, next);
|
||||
}
|
||||
}
|
||||
return [...byKey.values()];
|
||||
@@ -103,6 +137,35 @@ function askedKeysFromState(state: InferenceState, dossier: DecisionDossier): st
|
||||
])];
|
||||
}
|
||||
|
||||
function mappedAskableProbes(
|
||||
previous: InferenceState,
|
||||
eventProbes: readonly DiscriminatingEventProbe[],
|
||||
extra: readonly ConflictProbe[],
|
||||
dossier: DecisionDossier,
|
||||
): ConflictProbe[] {
|
||||
const asked = new Set(askedKeysFromState(previous, dossier));
|
||||
const existingKeys = new Set(previous.probes.map((item) => item.semantic_key));
|
||||
const askable: ConflictProbe[] = [];
|
||||
const incoming = [
|
||||
...extra.filter(isRefreshableDatedConflict),
|
||||
...eventProbes.flatMap((probe) => {
|
||||
if (!isRefreshableDatedProbe(probe)) return [];
|
||||
const mapped = probeFromEngine(probe);
|
||||
return mapped ? [mapped] : [];
|
||||
}),
|
||||
];
|
||||
for (const probe of incoming) {
|
||||
const aligned = alignedIncomingProbe(probe, previous.answered_probes);
|
||||
if (existingKeys.has(aligned.semantic_key)) continue;
|
||||
if (asked.has(aligned.semantic_key) || asked.has(aligned.id) || asked.has(aligned.candidate_split_hash)) {
|
||||
continue;
|
||||
}
|
||||
existingKeys.add(aligned.semantic_key);
|
||||
askable.push(aligned);
|
||||
}
|
||||
return askable;
|
||||
}
|
||||
|
||||
function activeCandidateTimes(state: InferenceState): string[] {
|
||||
return [...new Set(
|
||||
state.candidates
|
||||
@@ -166,7 +229,7 @@ async function defaultRefreshDiscriminatorProbes(
|
||||
state: withRefreshCount(
|
||||
input.state,
|
||||
nextCount,
|
||||
mergeConflictProbes(input.state.probes, incoming),
|
||||
mergeConflictProbes(input.state.probes, incoming, input.state.answered_probes),
|
||||
),
|
||||
eventProbes,
|
||||
candidateSetId: input.state.candidate_set_id,
|
||||
@@ -244,6 +307,40 @@ function shouldRefreshDatedPool(input: {
|
||||
return true;
|
||||
}
|
||||
|
||||
function refreshWriteBlockedReason(
|
||||
previous: InferenceState,
|
||||
result: RefreshDiscriminatorProbesResult,
|
||||
askableCount: number,
|
||||
): string | null {
|
||||
if (askableCount <= 0) return "no_new_probes";
|
||||
if (
|
||||
previous.candidates.length === 0
|
||||
|| !Array.isArray(result.state.candidates)
|
||||
|| result.state.candidates.length === 0
|
||||
) {
|
||||
return "empty_candidates";
|
||||
}
|
||||
if (
|
||||
result.state.candidate_set_id !== previous.candidate_set_id
|
||||
|| result.candidateSetId !== previous.candidate_set_id
|
||||
) {
|
||||
return "candidate_set_changed";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function refreshPersistBlockedReason(input: {
|
||||
previous: InferenceState;
|
||||
next: InferenceState;
|
||||
askableCount: number;
|
||||
}): string | null {
|
||||
if (input.askableCount <= 0) return "no_new_probes";
|
||||
if (input.next.candidates.length === 0) return "empty_candidates";
|
||||
if (input.previous.candidates.length === 0) return "empty_candidates";
|
||||
if (input.next.candidate_set_id !== input.previous.candidate_set_id) return "candidate_set_changed";
|
||||
return null;
|
||||
}
|
||||
|
||||
async function persistRefreshedInference(input: {
|
||||
accounting: AccountingClient;
|
||||
userId: string;
|
||||
@@ -251,9 +348,22 @@ async function persistRefreshedInference(input: {
|
||||
dossier: DecisionDossier;
|
||||
previous: InferenceState;
|
||||
next: InferenceState;
|
||||
}): Promise<void> {
|
||||
askableCount: number;
|
||||
}): Promise<boolean> {
|
||||
const blocked = refreshPersistBlockedReason(input);
|
||||
if (blocked) {
|
||||
console.warn(JSON.stringify({
|
||||
event: "rectification_refresh_persist_skipped",
|
||||
case_id: input.caseId,
|
||||
reason: blocked,
|
||||
candidates: input.next.candidates.length,
|
||||
candidate_set_id: input.next.candidate_set_id,
|
||||
refresh_count: input.next.refresh_count ?? 0,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
const last = input.next.answered_probes.at(-1);
|
||||
if (!last) return;
|
||||
if (!last) return false;
|
||||
const evidenceFp = input.dossier.latestResult?.evidenceLedgerFingerprint
|
||||
?? evidenceLedgerFingerprint(input.dossier.evidence as never);
|
||||
try {
|
||||
@@ -282,12 +392,14 @@ async function persistRefreshedInference(input: {
|
||||
idempotencyKey: `refresh_probes:${input.next.candidate_set_id}:${input.next.refresh_count ?? 1}`,
|
||||
candidateSetId: input.next.candidate_set_id,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[rectification-v9] persist refreshed probes failed case=${input.caseId} reason=${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,17 +433,44 @@ export async function refreshDatedDiscriminatorPoolIfNeeded(input: {
|
||||
dossier: input.dossier,
|
||||
state,
|
||||
});
|
||||
await persistRefreshedInference({
|
||||
const askable = mappedAskableProbes(state, result.eventProbes, result.state.probes, input.dossier);
|
||||
const blocked = refreshWriteBlockedReason(state, result, askable.length);
|
||||
if (blocked) {
|
||||
console.warn(JSON.stringify({
|
||||
event: "rectification_refresh_persist_skipped",
|
||||
case_id: input.caseId,
|
||||
reason: blocked,
|
||||
candidates: result.state.candidates.length,
|
||||
candidate_set_id: result.state.candidate_set_id,
|
||||
refresh_count: state.refresh_count ?? 0,
|
||||
}));
|
||||
return { dossier: input.dossier, state, refreshed: false };
|
||||
}
|
||||
const nextState: InferenceState = {
|
||||
...state,
|
||||
probes: mergeConflictProbes(state.probes, askable, state.answered_probes),
|
||||
refresh_count: (state.refresh_count ?? 0) + 1,
|
||||
refresh_answer_count: state.answered_probes.length,
|
||||
};
|
||||
const written = await persistRefreshedInference({
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
dossier: input.dossier,
|
||||
previous: state,
|
||||
next: result.state,
|
||||
next: nextState,
|
||||
askableCount: askable.length,
|
||||
});
|
||||
if (!written) {
|
||||
return { dossier: input.dossier, state, refreshed: false };
|
||||
}
|
||||
const persistableEventProbes = result.eventProbes.filter((probe) => {
|
||||
const key = probe.semantic_key?.trim() ?? `${probe.domain}.${probe.year}`;
|
||||
return askable.some((item) => item.semantic_key === key);
|
||||
});
|
||||
return {
|
||||
dossier: applyRefreshedProbesToDossier(input.dossier, result.state, result.eventProbes),
|
||||
state: result.state,
|
||||
dossier: applyRefreshedProbesToDossier(input.dossier, nextState, persistableEventProbes),
|
||||
state: nextState,
|
||||
refreshed: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
buildNextUserAction,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import {
|
||||
alignedProbeId,
|
||||
refreshDatedDiscriminatorPoolIfNeeded,
|
||||
resetRefreshDiscriminatorProbesForTests,
|
||||
setRefreshDiscriminatorProbesForTests,
|
||||
} from "../src/lib/rectification-agentic/v9/refresh-discriminator-probes.ts";
|
||||
@@ -471,6 +473,7 @@ function rpcDossier(decision: DecisionDossier, extra: {
|
||||
function idleHandlers(decision: DecisionDossier, extra: {
|
||||
activeFocus?: ReturnType<typeof activeFocusFixture> | null;
|
||||
throwOnFocus?: boolean;
|
||||
transitions?: Record<string, unknown>[];
|
||||
} = {}) {
|
||||
return fakeAccounting({
|
||||
...receiptHandlers,
|
||||
@@ -513,13 +516,27 @@ function idleHandlers(decision: DecisionDossier, extra: {
|
||||
}),
|
||||
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
|
||||
get_agentic_rectification_turn_receipt: () => null,
|
||||
append_agentic_rectification_inference_transition: (_fn, args) => ({
|
||||
result_id: "55555555-5555-4555-8555-555555555555",
|
||||
revision: Number(args.p_expected_revision ?? 0) + 1,
|
||||
idempotent: false,
|
||||
decision_receipt: {},
|
||||
decision_state_fingerprint: args.p_decision_state_fingerprint,
|
||||
}),
|
||||
append_agentic_rectification_inference_transition: (_fn, args) => {
|
||||
extra.transitions?.push(args as Record<string, unknown>);
|
||||
const inference = args.p_inference_state as {
|
||||
candidates?: unknown[];
|
||||
candidate_set_id?: string;
|
||||
refresh_count?: number;
|
||||
} | undefined;
|
||||
return {
|
||||
result_id: "55555555-5555-4555-8555-555555555555",
|
||||
revision: Number(args.p_expected_revision ?? 0) + 1,
|
||||
idempotent: false,
|
||||
decision_receipt: {
|
||||
inference_state: args.p_inference_state,
|
||||
},
|
||||
decision_state_fingerprint: args.p_decision_state_fingerprint,
|
||||
reason: args.p_reason,
|
||||
candidates: inference?.candidates?.length ?? 0,
|
||||
candidate_set_id: args.p_candidate_set_id ?? inference?.candidate_set_id,
|
||||
refresh_count: inference?.refresh_count ?? 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -775,3 +792,206 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
|
||||
assert.doesNotMatch(idle.hostNarration ?? "", /这次给出|最终/);
|
||||
assert.doesNotMatch(idle.hostNarration ?? "", /平时做事|月宿性格/);
|
||||
});
|
||||
|
||||
test("T0: GET-selected receipt probe is rejected until refresh merges it into inference_state", async () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
const base = accidentDossier(6);
|
||||
const mismatched = {
|
||||
...base,
|
||||
latestResult: {
|
||||
...base.latestResult!,
|
||||
decisionReceipt: {
|
||||
...(base.latestResult?.decisionReceipt ?? {}),
|
||||
discriminating_event_probes: [
|
||||
...((base.latestResult?.decisionReceipt?.discriminating_event_probes as DiscriminatingEventProbe[] | undefined) ?? []),
|
||||
eventProbeRow(FAMILY_REFRESH),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const getDecision = decideFromDossier(mismatched, { birthDate: "1997-08-08" });
|
||||
const getPlan = followupPlan(mismatched, getDecision.sessionOutcome);
|
||||
const getKey = getPlan.next_followup?.semantic_key ?? null;
|
||||
const persist = await persistServerOwnedFocus({
|
||||
accounting: idleHandlers(mismatched).client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: mismatched.latestResult?.decisionReceipt ?? null,
|
||||
followup: getPlan.next_followup,
|
||||
});
|
||||
console.log("T0 GET probe vs persist rejection", { getKey, persistStatus: persist.status });
|
||||
assert.equal(getKey, FAMILY_REFRESH.semantic_key, String(getKey));
|
||||
assert.equal(persist.status, "invalid_choice_schema", persist.status);
|
||||
});
|
||||
|
||||
test("T0: last inference row and GET probe key vs persist status after a real refresh", async () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
setRefreshDiscriminatorProbesForTests(async ({ state }) => {
|
||||
const nextCount = (state.refresh_count ?? 0) + 1;
|
||||
return {
|
||||
state: { ...state, refresh_count: nextCount },
|
||||
eventProbes: [eventProbeRow(FAMILY_REFRESH)],
|
||||
candidateSetId: state.candidate_set_id,
|
||||
refreshCount: nextCount,
|
||||
};
|
||||
});
|
||||
const dossier = accidentDossier(6);
|
||||
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
const transitions: Record<string, unknown>[] = [];
|
||||
const accounting = idleHandlers(dossier, { transitions });
|
||||
const next = await persistNextInterviewAfterChoice({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
dossier,
|
||||
decisionState: liveState(6),
|
||||
nextAction: publicNextAction(decision),
|
||||
decision,
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
const last = transitions.at(-1);
|
||||
const inference = last?.p_inference_state as {
|
||||
candidates?: unknown[];
|
||||
candidate_set_id?: string;
|
||||
refresh_count?: number;
|
||||
probes?: ReadonlyArray<{ id: string; semantic_key: string }>;
|
||||
} | undefined;
|
||||
const lastRow = {
|
||||
reason: last?.p_reason,
|
||||
candidates: inference?.candidates?.length ?? 0,
|
||||
candidate_set_id: last?.p_candidate_set_id ?? inference?.candidate_set_id,
|
||||
refresh_count: inference?.refresh_count ?? 0,
|
||||
};
|
||||
console.log("T0 last inference row", lastRow);
|
||||
assert.equal(lastRow.reason, "supersede");
|
||||
assert.equal(lastRow.candidates, TIMES.length);
|
||||
assert.equal(lastRow.candidate_set_id, liveState(6).candidate_set_id);
|
||||
assert.equal(lastRow.refresh_count, 1);
|
||||
const merged = inference?.probes?.find((item) => item.semantic_key === FAMILY_REFRESH.semantic_key);
|
||||
assert.equal(
|
||||
merged?.id,
|
||||
alignedProbeId(FAMILY_REFRESH, liveState(6).answered_probes),
|
||||
merged?.id,
|
||||
);
|
||||
const refreshedDossier = {
|
||||
...dossier,
|
||||
latestResult: {
|
||||
...dossier.latestResult!,
|
||||
decisionReceipt: {
|
||||
...(dossier.latestResult?.decisionReceipt ?? {}),
|
||||
inference_state: inference,
|
||||
discriminating_event_probes: [eventProbeRow(FAMILY_REFRESH)],
|
||||
},
|
||||
},
|
||||
};
|
||||
const getDecision = decideFromDossier(refreshedDossier, { birthDate: "1997-08-08" });
|
||||
const getPlan = followupPlan(refreshedDossier, getDecision.sessionOutcome);
|
||||
const getKey = getPlan.next_followup?.semantic_key ?? null;
|
||||
const persist = await persistServerOwnedFocus({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: refreshedDossier.latestResult?.decisionReceipt ?? null,
|
||||
followup: getPlan.next_followup,
|
||||
});
|
||||
console.log("T0 GET probe vs persist", { getKey, persistStatus: persist.status });
|
||||
assert.equal(getKey, FAMILY_REFRESH.semantic_key, String(getKey));
|
||||
assert.ok(
|
||||
persist.status === "created" || persist.status === "already_open",
|
||||
persist.status,
|
||||
);
|
||||
assert.equal(next.choiceReady, true, next.hostNarration);
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
});
|
||||
|
||||
test("T3: refresh without new engine probes does not write inference", async () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
setRefreshDiscriminatorProbesForTests(async ({ state }) => ({
|
||||
state: { ...state, refresh_count: (state.refresh_count ?? 0) + 1 },
|
||||
eventProbes: [],
|
||||
candidateSetId: state.candidate_set_id,
|
||||
refreshCount: (state.refresh_count ?? 0) + 1,
|
||||
}));
|
||||
const dossier = accidentDossier(6);
|
||||
const transitions: Record<string, unknown>[] = [];
|
||||
const idle = await persistNextInterviewIfIdle({
|
||||
accounting: idleHandlers(dossier, { transitions }).client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
});
|
||||
assert.equal(transitions.length, 0, JSON.stringify(transitions.at(-1) ?? {}));
|
||||
assert.ok((idle.hostNarration ?? "").trim());
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
});
|
||||
|
||||
test("T3: changed candidate set is not persisted even when engine returns a probe", async () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
setRefreshDiscriminatorProbesForTests(async ({ state }) => ({
|
||||
state: { ...state, candidate_set_id: "changed-set" },
|
||||
eventProbes: [eventProbeRow(FAMILY_REFRESH)],
|
||||
candidateSetId: "changed-set",
|
||||
refreshCount: 1,
|
||||
}));
|
||||
const dossier = accidentDossier(6);
|
||||
const transitions: Record<string, unknown>[] = [];
|
||||
await persistNextInterviewAfterChoice({
|
||||
accounting: idleHandlers(dossier, { transitions }).client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
dossier,
|
||||
decisionState: liveState(6),
|
||||
nextAction: publicNextAction(decideFromDossier(dossier, { birthDate: "1997-08-08" })),
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
assert.equal(transitions.length, 0);
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
});
|
||||
|
||||
test("T3: empty candidate list is not persisted even when engine returns a probe", async () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
setRefreshDiscriminatorProbesForTests(async ({ state }) => ({
|
||||
state: { ...state, candidates: [] },
|
||||
eventProbes: [eventProbeRow(FAMILY_REFRESH)],
|
||||
candidateSetId: state.candidate_set_id,
|
||||
refreshCount: 1,
|
||||
}));
|
||||
const dossier = accidentDossier(6);
|
||||
const transitions: Record<string, unknown>[] = [];
|
||||
const refreshed = await refreshDatedDiscriminatorPoolIfNeeded({
|
||||
accounting: idleHandlers(dossier, { transitions }).client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
dossier,
|
||||
state: liveState(6),
|
||||
hasDatedProbe: false,
|
||||
});
|
||||
assert.equal(refreshed.refreshed, false);
|
||||
assert.equal(transitions.length, 0);
|
||||
resetRefreshDiscriminatorProbesForTests();
|
||||
});
|
||||
|
||||
test("T3: merged probe ids follow the answered naming rule", () => {
|
||||
const hashedIncoming = {
|
||||
...FAMILY_REFRESH,
|
||||
id: `probe:${FAMILY_REFRESH.semantic_key}:${FAMILY_REFRESH.candidate_split_hash}`,
|
||||
};
|
||||
assert.equal(
|
||||
alignedProbeId(hashedIncoming, liveState(6).answered_probes),
|
||||
`probe:${FAMILY_REFRESH.semantic_key}`,
|
||||
);
|
||||
const hashedAnswers = liveState(6).answered_probes.map((item) => ({
|
||||
...item,
|
||||
probe_id: `probe:${item.semantic_key}:${item.candidate_split_hash}`,
|
||||
}));
|
||||
assert.equal(
|
||||
alignedProbeId(FAMILY_REFRESH, hashedAnswers),
|
||||
`probe:${FAMILY_REFRESH.semantic_key}:${FAMILY_REFRESH.candidate_split_hash}`,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user