Skip to content

Commit 9485d23

Browse files
committed
Remove LLM from recover_and_execute
1 parent 9903be5 commit 9485d23

5 files changed

Lines changed: 489 additions & 94 deletions

File tree

apps/web/src/app/api/telegram/webhook/route.test.ts

Lines changed: 63 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2-
import type { InboxPlanningOutput, RoutedTurn, TurnPolicyAction, TurnInterpretationType } from "@atlas/core";
2+
import type { InboxPlanningOutput, RoutedTurn, TurnPolicyAction, TurnInterpretationType, ConversationEntity } from "@atlas/core";
33
import {
44
getDefaultGoogleCalendarConnectionStore,
55
getDefaultFollowUpRuntimeStore,
@@ -17,6 +17,7 @@ import {
1717
resetInboxProcessingStoreForTests,
1818
resetIncomingTelegramIngressStoreForTests,
1919
seedInboxItemForProcessingTests,
20+
saveConversationState,
2021
updateOutgoingTelegramMessage
2122
} from "@atlas/db";
2223
import { getDefaultCalendarAdapter, resetCalendarAdapterForTests } from "@atlas/integrations";
@@ -133,6 +134,7 @@ function buildRoutedTurn(input: {
133134
confidence?: number;
134135
ambiguity?: "none" | "low" | "high";
135136
resolvedProposalId?: string;
137+
committedSlots?: Record<string, string | number>;
136138
}) : RoutedTurn {
137139
return {
138140
interpretation: {
@@ -154,11 +156,47 @@ function buildRoutedTurn(input: {
154156
: input.action === "execute_mutation"
155157
? { mutationInputSource: "direct_user_turn" as const }
156158
: {}),
157-
committedSlots: {}
159+
committedSlots: input.committedSlots ?? {}
158160
}
159161
};
160162
}
161163

164+
async function seedProposalEntity(input: {
165+
proposalId: string;
166+
originatingTurnText: string;
167+
missingSlots?: string[];
168+
replyText?: string;
169+
}) {
170+
const entity: ConversationEntity = {
171+
id: input.proposalId,
172+
conversationId: "conv-123",
173+
kind: "proposal_option",
174+
label: input.replyText ?? "Shall I schedule that?",
175+
status: "presented",
176+
createdAt: "2026-03-24T10:00:00Z",
177+
updatedAt: "2026-03-24T10:00:00Z",
178+
data: {
179+
route: "conversation_then_mutation",
180+
replyText: input.replyText ?? "Shall I schedule that?",
181+
originatingTurnText: input.originatingTurnText,
182+
missingSlots: input.missingSlots ?? []
183+
}
184+
};
185+
186+
await saveConversationState({
187+
userId: "123",
188+
entityRegistry: [entity],
189+
discourseState: {
190+
focus_entity_id: input.proposalId,
191+
currently_editable_entity_id: null,
192+
last_user_mentioned_entity_ids: [],
193+
last_presented_items: [],
194+
pending_clarifications: [],
195+
mode: "confirming"
196+
}
197+
});
198+
}
199+
162200
async function seedOutstandingFollowUpBundle(titles: string[]) {
163201
const store = getDefaultInboxProcessingStore();
164202
const followUpStore = getDefaultFollowUpRuntimeStore();
@@ -924,27 +962,13 @@ describe("telegram webhook route", () => {
924962
});
925963
});
926964

927-
it("treats confirmed mutation turns as write-capable when recovery finds one concrete proposal", async () => {
965+
it("treats confirmed mutation turns as write-capable when proposal entity has originating text", async () => {
928966
process.env.TELEGRAM_WEBHOOK_SECRET = "test-webhook-secret";
929-
await recordOutgoingTelegramMessageIfNew({
930-
userId: "123",
931-
eventType: "telegram_followup_message",
932-
idempotencyKey: "telegram:followup:proposal-confirmed",
933-
payload: {
934-
chatId: "999",
935-
text: "Would you like me to schedule it at 3pm?",
936-
attempts: 0
937-
},
938-
retryState: "sending"
939-
});
940-
await updateOutgoingTelegramMessage({
941-
idempotencyKey: "telegram:followup:proposal-confirmed",
942-
payload: {
943-
chatId: "999",
944-
text: "Would you like me to schedule it at 3pm?",
945-
attempts: 1
946-
},
947-
retryState: "sent"
967+
await seedProposalEntity({
968+
proposalId: "proposal-1",
969+
originatingTurnText: "Schedule the dentist reminder",
970+
missingSlots: ["time"],
971+
replyText: "Would you like me to schedule it at 3pm?"
948972
});
949973
const planner = vi.fn(async (): Promise<InboxPlanningOutput> => ({
950974
confidence: 0.9,
@@ -976,12 +1000,6 @@ describe("telegram webhook route", () => {
9761000
}
9771001
]
9781002
}));
979-
const confirmedMutationRecoverer = vi.fn(async () => ({
980-
outcome: "recovered" as const,
981-
recoveredText: "Schedule the dentist reminder at 3pm.",
982-
reason: "The user confirmed the concrete 3pm proposal.",
983-
userReplyMessage: "Got it - I've added 'Dentist reminder' to your schedule for today at 3pm."
984-
}));
9851003

9861004
const response = await handleTelegramWebhook(
9871005
buildRequest({
@@ -1010,11 +1028,11 @@ describe("telegram webhook route", () => {
10101028
conversationMemorySummarizer: async () => ({
10111029
summary: "The assistant proposed a concrete 3pm schedule and the user is now confirming it."
10121030
}),
1013-
confirmedMutationRecoverer,
10141031
turnRouter: async () => buildRoutedTurn({
10151032
turnType: "confirmation",
10161033
action: "recover_and_execute",
1017-
resolvedProposalId: "proposal-1"
1034+
resolvedProposalId: "proposal-1",
1035+
committedSlots: { time: "15:00" }
10181036
})
10191037
}
10201038
);
@@ -1027,32 +1045,11 @@ describe("telegram webhook route", () => {
10271045
outcome: "planned"
10281046
}
10291047
});
1030-
expect(confirmedMutationRecoverer).toHaveBeenCalledWith(
1031-
expect.objectContaining({
1032-
rawText: "Yes",
1033-
memorySummary: null,
1034-
entityRegistry: [],
1035-
discourseState: expect.objectContaining({
1036-
focus_entity_id: null,
1037-
last_user_mentioned_entity_ids: []
1038-
}),
1039-
recentTurns: expect.arrayContaining([
1040-
expect.objectContaining({
1041-
role: "assistant",
1042-
text: "Would you like me to schedule it at 3pm?"
1043-
}),
1044-
expect.objectContaining({
1045-
role: "user",
1046-
text: "Yes"
1047-
})
1048-
])
1049-
})
1050-
);
10511048
expect(planner).toHaveBeenCalledWith(
10521049
expect.objectContaining({
10531050
inboxItem: expect.objectContaining({
1054-
rawText: "Schedule the dentist reminder at 3pm.",
1055-
normalizedText: "Schedule the dentist reminder at 3pm."
1051+
rawText: "Schedule the dentist reminder at 3pm",
1052+
normalizedText: "Schedule the dentist reminder at 3pm"
10561053
})
10571054
})
10581055
);
@@ -1074,6 +1071,10 @@ describe("telegram webhook route", () => {
10741071

10751072
it("supports broader follow-up refinements in confirmed mutation turns", async () => {
10761073
process.env.TELEGRAM_WEBHOOK_SECRET = "test-webhook-secret";
1074+
await seedProposalEntity({
1075+
proposalId: "proposal-1",
1076+
originatingTurnText: "Move the scheduled review block 1 hour later"
1077+
});
10771078
const planner = vi.fn(async (): Promise<InboxPlanningOutput> => ({
10781079
confidence: 0.88,
10791080
summary: "Moved the scheduled review block one hour later.",
@@ -1161,12 +1162,6 @@ describe("telegram webhook route", () => {
11611162
conversationMemorySummarizer: async () => ({
11621163
summary: "The recent exchange includes one concrete proposal to move the existing review block one hour later."
11631164
}),
1164-
confirmedMutationRecoverer: async () => ({
1165-
outcome: "recovered",
1166-
recoveredText: "Move the scheduled review block 1 hour later.",
1167-
reason: "The user refined the recent concrete proposal.",
1168-
userReplyMessage: "Done - I've moved it to 4pm."
1169-
}),
11701165
turnRouter: async () => buildRoutedTurn({
11711166
turnType: "confirmation",
11721167
action: "recover_and_execute",
@@ -1186,7 +1181,7 @@ describe("telegram webhook route", () => {
11861181
expect(planner).toHaveBeenCalledWith(
11871182
expect.objectContaining({
11881183
inboxItem: expect.objectContaining({
1189-
rawText: "Move the scheduled review block 1 hour later."
1184+
rawText: "Move the scheduled review block 1 hour later"
11901185
})
11911186
})
11921187
);
@@ -1260,6 +1255,11 @@ describe("telegram webhook route", () => {
12601255
sendTelegramMessageMock.mockClear();
12611256
editTelegramMessageMock.mockClear();
12621257

1258+
await seedProposalEntity({
1259+
proposalId: "proposal-1",
1260+
originatingTurnText: "Mark the journaling session as done"
1261+
});
1262+
12631263
const response = await handleTelegramWebhook(
12641264
buildRequest({
12651265
update_id: 57,
@@ -1297,12 +1297,6 @@ describe("telegram webhook route", () => {
12971297
}
12981298
]
12991299
}),
1300-
confirmedMutationRecoverer: async () => ({
1301-
outcome: "recovered",
1302-
recoveredText: "Mark the journaling session as done.",
1303-
reason: "The latest turn clearly reports completion of the recent journaling task.",
1304-
userReplyMessage: "Got it."
1305-
}),
13061300
turnRouter: async () => buildRoutedTurn({
13071301
turnType: "confirmation",
13081302
action: "recover_and_execute",
@@ -1365,16 +1359,10 @@ describe("telegram webhook route", () => {
13651359
conversationMemorySummarizer: async () => ({
13661360
summary: "There were multiple possible recent proposals."
13671361
}),
1368-
confirmedMutationRecoverer: async () => ({
1369-
outcome: "needs_clarification",
1370-
recoveredText: null,
1371-
reason: "I have two recent proposals in view. Which one do you want me to apply?",
1372-
userReplyMessage: "I have two recent proposals in view. Which one do you want me to apply?"
1373-
}),
13741362
turnRouter: async () => buildRoutedTurn({
13751363
turnType: "confirmation",
13761364
action: "recover_and_execute",
1377-
resolvedProposalId: "proposal-1"
1365+
resolvedProposalId: "proposal-nonexistent"
13781366
})
13791367
}
13801368
);
@@ -1385,7 +1373,7 @@ describe("telegram webhook route", () => {
13851373
turnRoute: "confirmed_mutation",
13861374
processing: {
13871375
outcome: "conversation_replied",
1388-
reply: "I have two recent proposals in view. Which one do you want me to apply?"
1376+
reply: "I need a bit more detail to proceed. What would you like me to schedule?"
13891377
}
13901378
});
13911379
expect(planner).not.toHaveBeenCalled();
@@ -1400,7 +1388,7 @@ describe("telegram webhook route", () => {
14001388
expect.objectContaining({
14011389
chatId: "999",
14021390
messageId: 88,
1403-
text: "I have two recent proposals in view. Which one do you want me to apply?"
1391+
text: "I need a bit more detail to proceed. What would you like me to schedule?"
14041392
})
14051393
);
14061394
});

apps/web/src/lib/server/telegram-webhook.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
getTelegramAllowedUserIds,
66
isConfirmedMutationRecovered,
77
isTelegramUserAllowed,
8+
synthesizeMutationText,
89
type ConfirmedMutationRecoveryInput,
910
type ConfirmedMutationRecoveryOutput,
11+
type ConversationEntity,
1012
type ConversationTurn,
1113
type Task,
1214
getConfig,
@@ -407,28 +409,32 @@ export async function handleTelegramWebhook(
407409
userId: normalizedMessage.user.telegramUserId,
408410
action: routedWithContext.policy.action
409411
});
410-
const recoveredMutation = await (dependencies.confirmedMutationRecoverer ??
411-
recoverConfirmedMutationWithResponses)({
412-
rawText: normalizedMessage.rawText,
413-
normalizedText: normalizedMessage.normalizedText,
414-
recentTurns,
415-
memorySummary: conversationState?.conversation.summaryText ?? null,
416-
entityRegistry: conversationState?.entityRegistry ?? [],
417-
discourseState: conversationState?.discourseState ?? null
412+
const entityRegistry = conversationState?.entityRegistry ?? [];
413+
const proposalEntity = entityRegistry.find(
414+
(e): e is Extract<ConversationEntity, { kind: "proposal_option" }> =>
415+
e.kind === "proposal_option" && e.id === routedWithContext.policy.targetProposalId
416+
);
417+
418+
const synthesis = synthesizeMutationText({
419+
resolvedSlots: routedWithContext.policy.committedSlots,
420+
proposalEntity,
421+
entityRegistry
418422
});
419423
console.info("turn_recovery_result", {
420424
userId: normalizedMessage.user.telegramUserId,
421425
action: routedWithContext.policy.action,
422-
outcome: recoveredMutation.outcome
426+
outcome: synthesis.outcome
423427
});
424428

425-
if (recoveredMutation.outcome === "needs_clarification") {
429+
if (synthesis.outcome === "insufficient_data") {
430+
const clarificationMessage = "I need a bit more detail to proceed. What would you like me to schedule?";
431+
426432
if (conversationState) {
427433
await appendConversationTurn(
428434
{
429435
userId: normalizedMessage.user.telegramUserId,
430436
role: "assistant",
431-
text: recoveredMutation.userReplyMessage
437+
text: clarificationMessage
432438
},
433439
dependencies.conversationStateStore
434440
);
@@ -443,7 +449,7 @@ export async function handleTelegramWebhook(
443449
resolvedContract: routedWithContext.policy.resolvedContract
444450
},
445451
interpretation: routedWithContext.interpretation,
446-
reply: recoveredMutation.userReplyMessage,
452+
reply: clarificationMessage,
447453
userTurnText: normalizedMessage.rawText,
448454
summaryText: conversationState.conversation.summaryText
449455
})
@@ -457,7 +463,7 @@ export async function handleTelegramWebhook(
457463
userId: normalizedMessage.user.telegramUserId,
458464
chatId: normalizedMessage.chatId,
459465
inboxItemId: ingress.inboxItem.id,
460-
text: recoveredMutation.userReplyMessage
466+
text: clarificationMessage
461467
},
462468
{
463469
editor: dependencies.editor ?? editTelegramMessage,
@@ -473,7 +479,7 @@ export async function handleTelegramWebhook(
473479
routing: routedWithContext,
474480
processing: {
475481
outcome: "conversation_replied",
476-
reply: recoveredMutation.userReplyMessage
482+
reply: clarificationMessage
477483
}
478484
}
479485
}
@@ -482,15 +488,11 @@ export async function handleTelegramWebhook(
482488

483489
await dependencies.primeProcessingStore?.(ingress.inboxItem);
484490

485-
if (!isConfirmedMutationRecovered(recoveredMutation)) {
486-
throw new Error("Expected recovered mutation to include recoveredText");
487-
}
488-
489491
const processing = await processInboxItem(
490492
{
491493
inboxItemId: ingress.inboxItem.id,
492494
planningInboxTextOverride: {
493-
text: recoveredMutation.recoveredText
495+
text: synthesis.text
494496
}
495497
},
496498
{

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from "./commit-policy";
1313
export * from "./discourse-state";
1414
export * from "./proposal-rules";
1515
export * from "./slot-normalizer";
16+
export * from "./synthesize-mutation-text";
1617
export * from "./telegram";
1718
export * from "./write-contract";
1819

0 commit comments

Comments
 (0)