fix: limit CE question-id drift tolerance to recovery
This commit is contained in:
@@ -188,8 +188,8 @@ export interface CreateInteractiveAiSessionOptions {
|
||||
/**
|
||||
* Trust the caller's persisted/current question id when answering, even if a
|
||||
* rehydrated live handle generated a different question id while replaying.
|
||||
* Default remains strict for planning surfaces; CE enables this because its
|
||||
* persisted session row is the recovery anchor across dashboard restarts.
|
||||
* Default remains strict for fresh planning/CE sessions; recovery paths may
|
||||
* enable this when the persisted session row is the authoritative anchor.
|
||||
*/
|
||||
allowAnswerQuestionIdDrift?: boolean;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ describe("interrupt + resume (no silent loss)", () => {
|
||||
expect(done.event?.type).toBe("complete");
|
||||
expect(done.session.status).toBe("completed");
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(factory).toHaveBeenCalledWith(expect.objectContaining({ allowAnswerQuestionIdDrift: true }));
|
||||
expect(rehydrated.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(rehydrated.answer).toHaveBeenCalledTimes(1);
|
||||
const hasAnswerTurn = done.session.conversationHistory.some(
|
||||
@@ -183,6 +184,7 @@ describe("interrupt + resume (no silent loss)", () => {
|
||||
const done = await orch.answer(started.session.id, "q1", "a");
|
||||
expect(done.session.status).toBe("completed");
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(factory).toHaveBeenCalledWith(expect.not.objectContaining({ allowAnswerQuestionIdDrift: true }));
|
||||
expect(live.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(live.answer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -276,6 +278,7 @@ describe("interrupt + resume (no silent loss)", () => {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const after = store.get(created.id)!;
|
||||
expect(after.status).toBe("completed");
|
||||
expect(rehydrated.answer).toHaveBeenCalledTimes(1);
|
||||
const hasAnswerTurn = after.conversationHistory.some(
|
||||
(t) => t.text === JSON.stringify({ answer: "a", questionId: "q1" }),
|
||||
);
|
||||
|
||||
@@ -291,6 +291,7 @@ export class CeOrchestrator {
|
||||
private buildSessionOptions(
|
||||
stage: CeStageDefinition,
|
||||
sessionId: string,
|
||||
opts: { allowAnswerQuestionIdDrift?: boolean } = {},
|
||||
): Parameters<CreateInteractiveAiSessionFactory>[0] {
|
||||
const defaultProvider = getDefaultProvider(this.ctx.settings);
|
||||
const defaultModelId = getDefaultModelId(this.ctx.settings);
|
||||
@@ -302,7 +303,11 @@ export class CeOrchestrator {
|
||||
tools: "coding",
|
||||
requestedSkillNames: [stage.skillId],
|
||||
additionalSkillPaths,
|
||||
allowAnswerQuestionIdDrift: true,
|
||||
/*
|
||||
* FNXC:CompoundEngineering 2026-07-01-17:31:
|
||||
* Question-id drift tolerance is recovery-only. Fresh CE sessions keep the strict interactive seam guard so live/DB question divergence is surfaced immediately; rehydration enables the tolerance because the persisted session row is the recovery anchor after a restart.
|
||||
*/
|
||||
...(opts.allowAnswerQuestionIdDrift ? { allowAnswerQuestionIdDrift: true } : {}),
|
||||
onProgress: (event) => this.handleProgress(sessionId, event),
|
||||
...(defaultProvider ? { defaultProvider } : {}),
|
||||
...(defaultModelId ? { defaultModelId } : {}),
|
||||
@@ -643,7 +648,9 @@ export class CeOrchestrator {
|
||||
}
|
||||
|
||||
private async rehydrateReplay(session: CeSession, stage: CeStageDefinition): Promise<void> {
|
||||
const interactive = await this.factory!(this.buildSessionOptions(stage, session.id));
|
||||
const interactive = await this.factory!(
|
||||
this.buildSessionOptions(stage, session.id, { allowAnswerQuestionIdDrift: true }),
|
||||
);
|
||||
const live = interactive.session;
|
||||
|
||||
// Walk the recorded user turns in order. The FIRST user turn is the opening
|
||||
|
||||
Reference in New Issue
Block a user