Skip to content

Commit 4d80f10

Browse files
MaxLinCodeclaude
andcommitted
Fix double-prompting, ambiguous synthesis, and forward-action language bugs
Bug 1: When the LLM classifier confirms a turn without populating resolvedProposalId, the confirmation case now falls back to scanning the entity registry for a single active proposal instead of always re-presenting. Removes the double-prompting loop. Bug 3 (caused by Bug 1): Fixing Bug 1 prevents originatingTurnText from being set to the user's "Yes"/"Ok" confirmation text, which was causing synthesizeMutationText to produce "Yes" as the planner input and triggering the planner's ambiguity error. Bug 2: Add a safety rule to the conversation-response prompt blocking forward-action language like "I can proceed" or "I'll go ahead and" on the non-writing path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1c3de94 commit 4d80f10

3 files changed

Lines changed: 63 additions & 13 deletions

File tree

apps/web/src/lib/server/decide-turn-policy.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,47 @@ describe("decideTurnPolicy", () => {
169169
});
170170
});
171171

172+
it("recovers from registry when confirmation has no resolvedProposalId but one active proposal exists", () => {
173+
expect(
174+
decideTurnPolicy(
175+
input(
176+
{
177+
turnType: "confirmation",
178+
confidence: 0.95,
179+
// resolvedProposalId intentionally absent — classifier missed it
180+
},
181+
{},
182+
{
183+
rawText: "Yes",
184+
normalizedText: "Yes",
185+
recentTurns: [],
186+
entityRegistry: [
187+
{
188+
id: "proposal-1",
189+
conversationId: "conversation-1",
190+
kind: "proposal_option",
191+
label: "Schedule it at 3pm",
192+
status: "active",
193+
createdAt: "2026-03-20T16:00:00.000Z",
194+
updatedAt: "2026-03-20T16:00:00.000Z",
195+
data: {
196+
route: "conversation_then_mutation",
197+
replyText: "Would you like me to schedule it at 3pm?",
198+
confirmationRequired: true,
199+
slotSnapshot: {},
200+
},
201+
},
202+
],
203+
},
204+
),
205+
),
206+
).toMatchObject({
207+
action: "recover_and_execute",
208+
targetProposalId: "proposal-1",
209+
mutationInputSource: "recovered_proposal",
210+
});
211+
});
212+
172213
it("treats affirmative consent on a pending proposal as execution", () => {
173214
expect(
174215
decideTurnPolicy(

apps/web/src/lib/server/decide-turn-policy.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
type CommitPolicyOutput,
33
containsWriteVerb,
4+
type ConversationEntity,
45
deriveAmbiguity,
56
deriveConsentRequirement,
67
type TurnAmbiguity,
@@ -64,16 +65,20 @@ export function decideTurnPolicy(
6465
...(targetEntityId ? { targetEntityId } : {}),
6566
committedSlots: commitResult.committedSlots,
6667
};
67-
case "confirmation":
68-
if (classification.resolvedProposalId) {
68+
case "confirmation": {
69+
const proposalId =
70+
classification.resolvedProposalId ??
71+
resolveSingleActiveProposalId(input.routingContext.entityRegistry ?? []);
72+
73+
if (proposalId) {
6974
return {
7075
action: "recover_and_execute",
7176
reason: "The turn confirms one recoverable pending proposal.",
7277
requiresWrite: true,
7378
requiresConfirmation: false,
7479
useMutationPipeline: true,
7580
...(targetEntityId ? { targetEntityId } : {}),
76-
targetProposalId: classification.resolvedProposalId,
81+
targetProposalId: proposalId,
7782
mutationInputSource: "recovered_proposal",
7883
committedSlots: commitResult.committedSlots,
7984
};
@@ -82,22 +87,14 @@ export function decideTurnPolicy(
8287
return {
8388
action: "present_proposal",
8489
reason:
85-
"Confirmation language arrived for a ready consent-gated write, but no recoverable proposal exists yet; present proposal now.",
90+
"Confirmation language arrived but no recoverable proposal exists; present proposal now.",
8691
requiresWrite: true,
8792
requiresConfirmation: true,
8893
useMutationPipeline: false,
8994
clarificationSlots: [],
9095
committedSlots: commitResult.committedSlots,
9196
};
92-
// return {
93-
// action: "ask_clarification",
94-
// reason: "Confirmation language without one recoverable proposal should ask which proposal to apply.",
95-
// requiresWrite: false,
96-
// requiresConfirmation: false,
97-
// useMutationPipeline: false,
98-
// clarificationSlots: ["proposal"],
99-
// committedSlots: commitResult.committedSlots
100-
// };
97+
}
10198
case "clarification_answer":
10299
case "planning_request":
103100
case "edit_request":
@@ -210,6 +207,17 @@ function deriveStructuredWriteReadiness(
210207
};
211208
}
212209

210+
function resolveSingleActiveProposalId(
211+
registry: ConversationEntity[],
212+
): string | undefined {
213+
const active = registry.filter(
214+
(e) =>
215+
e.kind === "proposal_option" &&
216+
(e.status === "active" || e.status === "presented"),
217+
);
218+
return active.length === 1 ? active[0]!.id : undefined;
219+
}
220+
213221
function buildPolicyFromStructuredReadiness(
214222
readiness: StructuredWriteReadiness,
215223
targetEntityId: string | undefined,

packages/integrations/src/prompts/conversation-response.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const conversationResponseSystemPrompt = buildPromptSpec([
5353
"Do not make hard claims that any task, schedule, or reminder definitely exists or was created, updated, moved, completed, or archived.",
5454
"Do not present continuity context as authoritative Atlas state.",
5555
"Avoid first-person state claims like 'I created', 'I moved', 'I already have', or 'I haven't created yet' when you are reasoning only from continuity context.",
56+
"Do not use forward-action language like 'I can proceed', 'I'll go ahead and', 'I'm ready to', or 'I can do that now' that implies an imminent write on the non-writing path.",
5657
],
5758
},
5859
{

0 commit comments

Comments
 (0)