Skip to content

Commit 57b9dee

Browse files
committed
Make sure clarification answer emits new proposal
1 parent 987bb4a commit 57b9dee

3 files changed

Lines changed: 33 additions & 11 deletions

File tree

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ describe("decideTurnPolicy", () => {
415415
});
416416
});
417417

418-
it("routes compound confirmation (classified as clarification_answer) with enough info to execute_mutation when proposal ID cleared", () => {
418+
it("routes compound confirmation (classified as clarification_answer) with enough info to present_proposal for modified proposal", () => {
419419
const result = decideTurnPolicy(
420420
input(
421421
// resolvedProposalId is cleared by the guard — no stale proposal binding
@@ -446,11 +446,12 @@ describe("decideTurnPolicy", () => {
446446
)
447447
);
448448

449+
// Modified proposal still requires consent — emits new proposal, not the old ID
449450
expect(result).toMatchObject({
450-
action: "execute_mutation",
451-
mutationInputSource: "direct_user_turn"
451+
action: "present_proposal",
452+
requiresWrite: true,
453+
requiresConfirmation: true
452454
});
453-
expect(result.targetProposalId).toBeUndefined();
454455
});
455456

456457
it("routes compound confirmation (classified as clarification_answer) with missing slots to ask_clarification", () => {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ export function decideTurnPolicy(input: DecideTurnPolicyInput): TurnPolicyDecisi
104104
);
105105
case "unknown": {
106106
const isNonWrite = ambiguity === "none" && !containsWriteVerb(input.routingContext.normalizedText);
107-
const clarificationSlots = [
107+
const clarificationSlots = Array.from(new Set([
108108
...commitResult.missingSlots,
109109
...commitResult.needsClarification
110-
];
110+
]));
111111
return {
112112
action: isNonWrite ? "reply_only" : "ask_clarification",
113113
reason: isNonWrite
@@ -128,10 +128,10 @@ function deriveStructuredWriteReadiness(
128128
ambiguity: TurnAmbiguity
129129
): StructuredWriteReadiness {
130130
const { classification, commitResult } = input;
131-
const allClarificationSlots = [
131+
const allClarificationSlots = Array.from(new Set([
132132
...commitResult.missingSlots,
133133
...commitResult.needsClarification
134-
];
134+
]));
135135

136136
if (ambiguity === "high") {
137137
return {
@@ -218,7 +218,7 @@ function buildPolicyFromStructuredReadiness(
218218
return {
219219
action: "present_proposal",
220220
reason: readiness.reason,
221-
requiresWrite: false,
221+
requiresWrite: true,
222222
requiresConfirmation: true,
223223
useMutationPipeline: false,
224224
...(targetEntityId ? { targetEntityId } : {}),

packages/core/src/proposal-rules.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,25 @@ export type ConsentRequirementInput = {
1414

1515
export function deriveConsentRequirement(input: ConsentRequirementInput) {
1616
const { classification } = input;
17-
const activeProposal = input.entityRegistry.find(
17+
18+
const consentRequiringProposals = input.entityRegistry.filter(
1819
(entity): entity is ProposalOption =>
1920
entity.kind === "proposal_option" &&
2021
(entity.status === "active" || entity.status === "presented") &&
21-
entity.id === classification.resolvedProposalId &&
2222
entity.data.confirmationRequired === true
2323
);
2424

25+
// Match by resolvedProposalId if available, otherwise fall back to the
26+
// single active/presented proposal (covers modified-proposal case where
27+
// the guard cleared resolvedProposalId).
28+
const matchedById = classification.resolvedProposalId
29+
? consentRequiringProposals.find((p) => p.id === classification.resolvedProposalId)
30+
: undefined;
31+
const inferredProposal = !matchedById && consentRequiringProposals.length === 1
32+
? consentRequiringProposals[0]
33+
: undefined;
34+
const activeProposal = matchedById ?? inferredProposal;
35+
2536
if (!activeProposal) {
2637
return {
2738
required: false as const,
@@ -49,6 +60,16 @@ export function deriveConsentRequirement(input: ConsentRequirementInput) {
4960
};
5061
}
5162

63+
// When the proposal was inferred (not matched by ID), consent is required
64+
// but we do NOT return targetProposalId — the caller should emit a new
65+
// proposal rather than resurrecting the old one for direct execution.
66+
if (inferredProposal) {
67+
return {
68+
required: true as const,
69+
reason: "Write request is ready, but the proposal was modified and needs fresh consent."
70+
};
71+
}
72+
5273
return {
5374
required: true as const,
5475
reason: "Write request is ready, but deterministic product policy still requires user consent.",

0 commit comments

Comments
 (0)