Skip to content

Commit 3c93449

Browse files
authored
Merge pull request #51 from MaxLinCode/codex/fix-unconditional-clarification
Fix unconditionally persisted clarification
2 parents 202de8d + 9b23962 commit 3c93449

3 files changed

Lines changed: 389 additions & 63 deletions

File tree

Lines changed: 233 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expect, it } from "vitest";
2+
import type { ConversationStateSnapshot } from "@atlas/core";
3+
import type { ProcessedInboxResult } from "@atlas/db";
24

3-
import { deriveConversationReplyState } from "./conversation-state";
5+
import { deriveConversationReplyState, deriveMutationState } from "./conversation-state";
46

5-
function buildSnapshot() {
7+
function buildSnapshot(): ConversationStateSnapshot {
68
return {
79
conversation: {
810
id: "conversation-1",
@@ -21,86 +23,283 @@ function buildSnapshot() {
2123
last_user_mentioned_entity_ids: [],
2224
last_presented_items: [],
2325
pending_clarifications: [],
24-
mode: "clarifying" as const
26+
mode: "planning" as const
2527
}
2628
};
2729
}
2830

2931
describe("deriveConversationReplyState", () => {
30-
it("keeps clarifications as clarifications when required slots are still missing", () => {
32+
it("persists a clarification only when structured interpretation still has a real missing slot", () => {
3133
const result = deriveConversationReplyState({
3234
snapshot: buildSnapshot(),
33-
policyAction: "ask_clarification",
35+
policy: {
36+
action: "ask_clarification"
37+
},
3438
interpretation: {
3539
turnType: "planning_request",
3640
confidence: 0.58,
3741
resolvedEntityIds: [],
3842
ambiguity: "high",
3943
missingSlots: ["time"]
4044
},
41-
reply: "It sounds like you want me to block out time for planning your Malaysia trip. I can proceed with that now once I know the time.",
45+
reply: "What time should I schedule the Malaysia trip planning?",
4246
userTurnText: "schedule malaysia trip planning tomorrow",
4347
summaryText: null,
4448
occurredAt: "2026-03-22T16:05:00.000Z"
4549
});
4650

47-
expect(result.mode).toBe("conversation_then_mutation");
4851
expect(result.entityRegistry).toHaveLength(1);
4952
expect(result.entityRegistry[0]).toMatchObject({
5053
kind: "clarification",
51-
status: "active"
54+
status: "active",
55+
data: {
56+
reason: "time"
57+
}
5258
});
59+
expect(result.discourseState?.pending_clarifications).toEqual([
60+
expect.objectContaining({
61+
slot: "time",
62+
status: "pending",
63+
blocking: true
64+
})
65+
]);
5366
expect(result.discourseState?.mode).toBe("clarifying");
5467
});
5568

56-
it("does not upgrade to proposal based on wording alone", () => {
69+
it("does not persist clarification state for ask_clarification without a real blocking slot", () => {
5770
const result = deriveConversationReplyState({
5871
snapshot: buildSnapshot(),
59-
policyAction: "ask_clarification",
72+
policy: {
73+
action: "ask_clarification",
74+
clarificationSlots: ["proposal"]
75+
},
6076
interpretation: {
61-
turnType: "planning_request",
62-
confidence: 0.91,
77+
turnType: "confirmation",
78+
confidence: 0.32,
6379
resolvedEntityIds: [],
64-
ambiguity: "none"
80+
ambiguity: "high"
6581
},
66-
reply: "It sounds like you want me to block out time for planning your Malaysia trip at 3:15 PM for 15 minutes. I can proceed with that now.",
67-
userTurnText: "schedule malaysia trip planning at 3:15pm for 15 minutes",
82+
reply: "Which proposal do you want me to apply?",
83+
userTurnText: "ok",
6884
summaryText: null,
6985
occurredAt: "2026-03-22T16:05:00.000Z"
7086
});
7187

72-
expect(result.entityRegistry[0]).toMatchObject({
73-
kind: "clarification"
74-
});
75-
expect(result.discourseState?.mode).toBe("clarifying");
88+
expect(result.entityRegistry).toHaveLength(0);
89+
expect(result.discourseState?.pending_clarifications).toEqual([]);
90+
expect(result.discourseState?.mode).toBe("planning");
7691
});
7792

78-
it("persists proposal state only when policy already says present_proposal", () => {
93+
it("rejects slot unknown as a persistable blocking clarification", () => {
7994
const result = deriveConversationReplyState({
8095
snapshot: buildSnapshot(),
81-
policyAction: "present_proposal",
96+
policy: {
97+
action: "ask_clarification"
98+
},
8299
interpretation: {
83-
turnType: "edit_request",
84-
confidence: 0.94,
100+
turnType: "unknown",
101+
confidence: 0.42,
102+
resolvedEntityIds: [],
103+
ambiguity: "high",
104+
missingSlots: ["unknown"]
105+
},
106+
reply: "Can you clarify what you want me to change?",
107+
userTurnText: "do it",
108+
summaryText: null,
109+
occurredAt: "2026-03-22T16:05:00.000Z"
110+
});
111+
112+
expect(result.entityRegistry).toHaveLength(0);
113+
expect(result.discourseState?.pending_clarifications).toEqual([]);
114+
expect(result.discourseState?.mode).toBe("planning");
115+
});
116+
117+
it("clears active clarification state when a proposal is presented", () => {
118+
const snapshot = buildSnapshot();
119+
snapshot.entityRegistry = [
120+
{
121+
id: "clar-1",
122+
conversationId: "conversation-1",
123+
kind: "clarification",
124+
label: "Need a time",
125+
status: "active",
126+
createdAt: "2026-03-22T16:01:00.000Z",
127+
updatedAt: "2026-03-22T16:01:00.000Z",
128+
data: {
129+
prompt: "What time should I schedule it?",
130+
reason: "time",
131+
open: true
132+
}
133+
}
134+
];
135+
snapshot.discourseState = {
136+
focus_entity_id: "clar-1",
137+
currently_editable_entity_id: null,
138+
last_user_mentioned_entity_ids: [],
139+
last_presented_items: [],
140+
pending_clarifications: [
141+
{
142+
id: "clar-1",
143+
slot: "time",
144+
question: "What time should I schedule it?",
145+
status: "pending",
146+
blocking: true,
147+
createdAt: "2026-03-22T16:01:00.000Z",
148+
createdTurnId: "assistant:1"
149+
}
150+
],
151+
mode: "clarifying"
152+
};
153+
154+
const result = deriveConversationReplyState({
155+
snapshot,
156+
policy: {
157+
action: "present_proposal"
158+
},
159+
interpretation: {
160+
turnType: "clarification_answer",
161+
confidence: 0.93,
85162
resolvedEntityIds: ["task-1"],
86163
ambiguity: "none"
87164
},
88-
reply: "I can move the planning block to 3:15 PM for 15 minutes. Would you like me to do that now?",
89-
userTurnText: "move it to 3:15pm for 15 minutes",
165+
reply: "I can schedule the Malaysia trip planning at 5 PM. Want me to do that now?",
166+
userTurnText: "5pm",
90167
summaryText: null,
91168
occurredAt: "2026-03-22T16:05:00.000Z"
92169
});
93170

94-
expect(result.entityRegistry).toHaveLength(1);
95-
expect(result.entityRegistry[0]).toMatchObject({
96-
kind: "proposal_option",
97-
status: "active",
98-
data: {
99-
targetEntityId: "task-1",
100-
policyAction: "present_proposal",
101-
confirmationRequired: true
171+
expect(result.entityRegistry).toEqual(
172+
expect.arrayContaining([
173+
expect.objectContaining({
174+
id: "clar-1",
175+
kind: "clarification",
176+
status: "resolved"
177+
}),
178+
expect.objectContaining({
179+
kind: "proposal_option",
180+
status: "active"
181+
})
182+
])
183+
);
184+
expect(result.discourseState?.pending_clarifications).toEqual([
185+
expect.objectContaining({
186+
id: "clar-1",
187+
status: "resolved"
188+
})
189+
]);
190+
expect(result.discourseState?.mode).toBe("confirming");
191+
});
192+
});
193+
194+
describe("deriveMutationState", () => {
195+
it("clears pending clarifications after execution", () => {
196+
const snapshot = buildSnapshot();
197+
snapshot.entityRegistry = [
198+
{
199+
id: "clar-1",
200+
conversationId: "conversation-1",
201+
kind: "clarification",
202+
label: "Need a time",
203+
status: "active",
204+
createdAt: "2026-03-22T16:01:00.000Z",
205+
updatedAt: "2026-03-22T16:01:00.000Z",
206+
data: {
207+
prompt: "What time should I schedule it?",
208+
reason: "time",
209+
open: true
210+
}
102211
}
212+
];
213+
snapshot.discourseState = {
214+
focus_entity_id: "clar-1",
215+
currently_editable_entity_id: null,
216+
last_user_mentioned_entity_ids: [],
217+
last_presented_items: [],
218+
pending_clarifications: [
219+
{
220+
id: "clar-1",
221+
slot: "time",
222+
question: "What time should I schedule it?",
223+
status: "pending",
224+
blocking: true,
225+
createdAt: "2026-03-22T16:01:00.000Z",
226+
createdTurnId: "assistant:1"
227+
}
228+
],
229+
mode: "clarifying"
230+
};
231+
232+
const processing: ProcessedInboxResult = {
233+
outcome: "planned",
234+
inboxItem: {
235+
id: "inbox-1",
236+
userId: "user-1",
237+
rawText: "schedule malaysia trip planning",
238+
normalizedText: "schedule malaysia trip planning",
239+
processingStatus: "planned",
240+
linkedTaskIds: ["task-1"],
241+
createdAt: "2026-03-22T16:00:00.000Z"
242+
},
243+
plannerRun: {
244+
id: "run-1",
245+
userId: "user-1",
246+
inboxItemId: "inbox-1",
247+
version: "test",
248+
modelInput: {},
249+
modelOutput: {},
250+
confidence: 0.92
251+
},
252+
createdTasks: [
253+
{
254+
id: "task-1",
255+
userId: "user-1",
256+
sourceInboxItemId: "inbox-1",
257+
lastInboxItemId: "inbox-1",
258+
title: "Malaysia trip planning",
259+
lifecycleState: "scheduled",
260+
externalCalendarEventId: null,
261+
externalCalendarId: null,
262+
scheduledStartAt: "2026-03-23T00:00:00.000Z",
263+
scheduledEndAt: "2026-03-23T00:30:00.000Z",
264+
calendarSyncStatus: "in_sync",
265+
calendarSyncUpdatedAt: null,
266+
rescheduleCount: 0,
267+
lastFollowupAt: null,
268+
followupReminderSentAt: null,
269+
completedAt: null,
270+
archivedAt: null,
271+
priority: "medium",
272+
urgency: "medium"
273+
}
274+
],
275+
scheduleBlocks: [
276+
{
277+
id: "block-1",
278+
userId: "user-1",
279+
taskId: "task-1",
280+
startAt: "2026-03-23T00:00:00.000Z",
281+
endAt: "2026-03-23T00:30:00.000Z",
282+
confidence: 0.92,
283+
reason: "User requested 5 PM.",
284+
rescheduleCount: 0,
285+
externalCalendarId: null
286+
}
287+
],
288+
followUpMessage: "Scheduled it for 5 PM."
289+
};
290+
291+
const result = deriveMutationState({
292+
snapshot,
293+
processing,
294+
occurredAt: "2026-03-22T16:10:00.000Z"
103295
});
104-
expect(result.discourseState?.mode).toBe("confirming");
296+
297+
expect(result.discourseState.pending_clarifications).toEqual([
298+
expect.objectContaining({
299+
id: "clar-1",
300+
status: "resolved"
301+
})
302+
]);
303+
expect(result.discourseState.mode).toBe("editing");
105304
});
106305
});

0 commit comments

Comments
 (0)