Skip to content

Commit 0adec17

Browse files
MaxLinCodeclaude
andcommitted
feat: collapse webhook mutation branches to single executePendingWrite call
Remove the recover_and_execute branch and processInboxItem usage from the webhook handler. All mutation execution now flows through the deterministic executePendingWrite orchestrator, loading context from the store at execution time. Also fix rehydrate-proposal null/undefined type mismatches and update turn-router tests to match the simplified policy schema. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fd72356 commit 0adec17

5 files changed

Lines changed: 44 additions & 199 deletions

File tree

apps/web/src/lib/server/rehydrate-proposal.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("rehydratePendingWriteFromProposal", () => {
1414
route: "conversation_then_mutation" as const,
1515
replyText: "Schedule gym at 5pm?",
1616
fieldSnapshot: {
17-
scheduleFields: { day: "2026-04-06", time: { hour: 17, minute: 0 }, duration: 60 },
17+
scheduleFields: { day: "2026-04-06", time: { kind: "absolute" as const, hour: 17, minute: 0 }, duration: 60 },
1818
taskFields: { priority: "medium" },
1919
},
2020
targetEntityId: "task-123",
@@ -31,7 +31,7 @@ describe("rehydratePendingWriteFromProposal", () => {
3131
expect(result!.targetRef).toEqual({
3232
entityId: "task-123",
3333
description: "Schedule gym",
34-
entityKind: null,
34+
entityKind: undefined,
3535
});
3636
expect(result!.resolvedFields).toEqual(baseProposal.data.fieldSnapshot);
3737
expect(result!.originatingText).toBe("schedule gym tomorrow at 5pm");
@@ -56,9 +56,9 @@ describe("rehydratePendingWriteFromProposal", () => {
5656

5757
expect(result).not.toBeNull();
5858
expect(result!.targetRef).toEqual({
59-
entityId: null,
59+
entityId: undefined,
6060
description: "Schedule gym",
61-
entityKind: null,
61+
entityKind: undefined,
6262
});
6363
});
6464

apps/web/src/lib/server/rehydrate-proposal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export function rehydratePendingWriteFromProposal(
1414
return {
1515
operationKind: data.operationKind,
1616
targetRef: {
17-
entityId: data.targetEntityId ?? null,
17+
entityId: data.targetEntityId ?? undefined,
1818
description: proposal.label,
19-
entityKind: null,
19+
entityKind: undefined,
2020
},
2121
resolvedFields: data.fieldSnapshot,
2222
missingFields: data.missingFields ?? [],

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

Lines changed: 29 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import {
2+
buildDefaultUserProfile,
23
buildTelegramFollowUpIdempotencyKey,
34
buildTelegramWebhookIdempotencyKey,
4-
type ConfirmedMutationRecoveryInput,
5-
type ConfirmedMutationRecoveryOutput,
65
type ConversationEntity,
76
type ConversationTurn,
87
getAppBaseUrl,
98
getConfig,
109
getTelegramAllowedUserIds,
11-
isConfirmedMutationRecovered,
1210
isTelegramUserAllowed,
1311
normalizeTelegramUpdate,
14-
synthesizeMutationText,
1512
type Task,
1613
telegramUpdateSchema,
1714
} from "@atlas/core";
@@ -36,7 +33,6 @@ import {
3633
type ConversationMemorySummaryInput,
3734
type ConversationMemorySummaryOutput,
3835
editTelegramMessage,
39-
recoverConfirmedMutationWithResponses,
4036
sendTelegramChatAction,
4137
sendTelegramMessage,
4238
summarizeConversationMemoryWithResponses,
@@ -58,10 +54,8 @@ import {
5854
hasActiveGoogleCalendarConnection,
5955
} from "./google-calendar";
6056
import { renderMutationReply } from "./mutation-reply";
61-
import {
62-
type ProcessInboxItemDependencies,
63-
processInboxItem,
64-
} from "./process-inbox-item";
57+
import { type ProcessInboxItemDependencies } from "./process-inbox-item";
58+
import { executePendingWrite } from "./execute-pending-write";
6559
import { sendTelegramMessageWithPersistence } from "./telegram-webhook-transport";
6660
import {
6761
doesPolicyAllowWrites,
@@ -94,9 +88,6 @@ type TelegramWebhookDependencies = ProcessInboxItemDependencies & {
9488
conversationMemorySummarizer?: (
9589
input: ConversationMemorySummaryInput,
9690
) => Promise<ConversationMemorySummaryOutput>;
97-
confirmedMutationRecoverer?: (
98-
input: ConfirmedMutationRecoveryInput,
99-
) => Promise<ConfirmedMutationRecoveryOutput>;
10091
googleCalendarConnectionChecker?: (userId: string) => Promise<boolean>;
10192
googleCalendarConnectLinkBuilder?: (input: {
10293
baseUrl: string;
@@ -440,69 +431,24 @@ export async function handleTelegramWebhook(
440431
};
441432
}
442433

443-
if (routedWithContext.policy.action === "recover_and_execute") {
434+
if (
435+
routedWithContext.policy.action === "execute_mutation"
436+
) {
444437
console.info("turn_execution_branch", {
445438
userId: normalizedMessage.user.telegramUserId,
446439
action: routedWithContext.policy.action,
447440
});
448-
const entityRegistry = conversationState?.entityRegistry ?? [];
449-
const proposalEntity = entityRegistry.find(
450-
(e): e is Extract<ConversationEntity, { kind: "proposal_option" }> =>
451-
e.kind === "proposal_option" &&
452-
e.id === routedWithContext.policy.targetProposalId,
453-
);
454441

455-
const synthesis = synthesizeMutationText({
456-
resolvedFields:
457-
routedWithContext.policy.resolvedOperation?.resolvedFields ?? {},
458-
targetEntityId: routedWithContext.policy.targetEntityId,
459-
proposalEntity,
460-
entityRegistry,
461-
});
462-
console.info("turn_recovery_result", {
463-
userId: normalizedMessage.user.telegramUserId,
464-
action: routedWithContext.policy.action,
465-
outcome: synthesis.outcome,
466-
});
467-
468-
if (synthesis.outcome === "insufficient_data") {
469-
const clarificationMessage =
470-
"I need a bit more detail to proceed. What would you like me to schedule?";
471-
472-
if (conversationState) {
473-
await appendConversationTurn(
474-
{
475-
userId: normalizedMessage.user.telegramUserId,
476-
role: "assistant",
477-
text: clarificationMessage,
478-
},
479-
dependencies.conversationStateStore,
480-
);
481-
await saveConversationState(
482-
{
483-
userId: normalizedMessage.user.telegramUserId,
484-
...deriveConversationReplyState({
485-
snapshot: conversationState,
486-
policy: {
487-
action: "ask_clarification",
488-
resolvedOperation: routedWithContext.policy.resolvedOperation,
489-
},
490-
interpretation: routedWithContext.interpretation,
491-
reply: clarificationMessage,
492-
userTurnText: normalizedMessage.rawText,
493-
summaryText: conversationState.conversation.summaryText,
494-
}),
495-
},
496-
dependencies.conversationStateStore,
497-
);
498-
}
442+
await dependencies.primeProcessingStore?.(ingress.inboxItem);
499443

444+
const resolvedOperation = routedWithContext.policy.resolvedOperation;
445+
if (!resolvedOperation) {
500446
return replyWithText(
501447
{
502448
userId: normalizedMessage.user.telegramUserId,
503449
chatId: normalizedMessage.chatId,
504450
inboxItemId: ingress.inboxItem.id,
505-
text: clarificationMessage,
451+
text: "I need a bit more detail to proceed.",
506452
},
507453
{
508454
editor: dependencies.editor ?? editTelegramMessage,
@@ -518,32 +464,29 @@ export async function handleTelegramWebhook(
518464
inboxItem: ingress.inboxItem,
519465
turnRoute: compatibilityTurnRoute,
520466
routing: routedWithContext,
521-
processing: {
522-
outcome: "conversation_replied",
523-
reply: clarificationMessage,
524-
},
467+
processing: { outcome: "needs_clarification" },
525468
},
526469
},
527470
);
528471
}
529472

530-
await dependencies.primeProcessingStore?.(ingress.inboxItem);
531-
532-
const processing = await processInboxItem(
533-
{
534-
inboxItemId: ingress.inboxItem.id,
535-
planningInboxTextOverride: {
536-
text: synthesis.text,
537-
},
538-
},
539-
{
540-
...(dependencies.store ? { store: dependencies.store } : {}),
541-
...(dependencies.planner ? { planner: dependencies.planner } : {}),
542-
...(dependencies.calendar !== undefined
543-
? { calendar: dependencies.calendar }
544-
: {}),
545-
},
473+
const executionStore = dependencies.store ?? getDefaultInboxProcessingStore();
474+
const executionContext = await executionStore.loadContext(
475+
ingress.inboxItem.id,
546476
);
477+
478+
const processing = await executePendingWrite({
479+
pendingWriteOperation: resolvedOperation,
480+
userId: normalizedMessage.user.telegramUserId,
481+
tasks: executionContext?.tasks ?? [],
482+
scheduleBlocks: executionContext?.scheduleBlocks ?? [],
483+
userProfile: executionContext?.userProfile ?? buildDefaultUserProfile(normalizedMessage.user.telegramUserId),
484+
calendar: dependencies.calendar ?? null,
485+
googleCalendarConnection:
486+
executionContext?.googleCalendarConnection ?? null,
487+
store: executionStore,
488+
});
489+
547490
const outboundDelivery = await finalizeFollowUpMessage(
548491
{
549492
userId: normalizedMessage.user.telegramUserId,
@@ -606,83 +549,9 @@ export async function handleTelegramWebhook(
606549
};
607550
}
608551

609-
console.info("turn_execution_branch", {
610-
userId: normalizedMessage.user.telegramUserId,
611-
action: routedWithContext.policy.action,
612-
});
613-
await dependencies.primeProcessingStore?.(ingress.inboxItem);
614-
615-
const processing = await processInboxItem(
616-
{
617-
inboxItemId: ingress.inboxItem.id,
618-
},
619-
{
620-
...(dependencies.store ? { store: dependencies.store } : {}),
621-
...(dependencies.planner ? { planner: dependencies.planner } : {}),
622-
...(dependencies.calendar !== undefined
623-
? { calendar: dependencies.calendar }
624-
: {}),
625-
},
626-
);
627-
const outboundDelivery = await finalizeFollowUpMessage(
628-
{
629-
userId: normalizedMessage.user.telegramUserId,
630-
chatId: normalizedMessage.chatId,
631-
inboxItemId: ingress.inboxItem.id,
632-
text: processing.followUpMessage,
633-
},
634-
{
635-
editor: dependencies.editor ?? editTelegramMessage,
636-
placeholderDelivery,
637-
sender: dependencies.sender ?? sendTelegramMessage,
638-
...(dependencies.deliveryStore
639-
? { deliveryStore: dependencies.deliveryStore }
640-
: {}),
641-
},
642-
);
643-
const followUpContinuation = await maybeSendOutstandingFollowUpContinuation(
644-
{
645-
userId: normalizedMessage.user.telegramUserId,
646-
chatId: normalizedMessage.chatId,
647-
inboxItemId: ingress.inboxItem.id,
648-
},
649-
dependencies,
650-
);
651-
652-
if (conversationState) {
653-
await appendConversationTurn(
654-
{
655-
userId: normalizedMessage.user.telegramUserId,
656-
role: "assistant",
657-
text: processing.followUpMessage,
658-
},
659-
dependencies.conversationStateStore,
660-
);
661-
await saveConversationState(
662-
{
663-
userId: normalizedMessage.user.telegramUserId,
664-
...deriveMutationState({
665-
snapshot: conversationState,
666-
processing,
667-
}),
668-
},
669-
dependencies.conversationStateStore,
670-
);
671-
}
672-
673552
return {
674-
status: 200,
675-
body: {
676-
accepted: true,
677-
idempotencyKey,
678-
ingestion: normalizedMessage,
679-
inboxItem: ingress.inboxItem,
680-
turnRoute: compatibilityTurnRoute,
681-
routing: routedWithContext,
682-
processing,
683-
outboundDelivery,
684-
followUpContinuation,
685-
},
553+
status: 500,
554+
body: { accepted: false, error: "unhandled_policy_action" },
686555
};
687556
}
688557

@@ -1043,23 +912,11 @@ async function tryHandleFollowUpReply(
1043912

1044913
const directStore = dependencies.store ?? getDefaultInboxProcessingStore();
1045914
await dependencies.primeProcessingStore?.(input.inboxItem);
1046-
const plannerRun = {
1047-
userId: input.inboxItem.userId,
1048-
inboxItemId: input.inboxItem.id,
1049-
version: "followup-direct-v1",
1050-
modelInput: {
1051-
source: "followup_reply",
1052-
text: input.normalizedMessage.normalizedText,
1053-
},
1054-
modelOutput: parsed,
1055-
confidence: 1,
1056-
};
1057915

1058916
if (parsed.kind === "done") {
1059917
const processing = await directStore.saveTaskCompletionResult({
1060918
inboxItemId: input.inboxItem.id,
1061919
confidence: 1,
1062-
plannerRun,
1063920
taskIds: selectedTasks.map((task) => task.id),
1064921
followUpMessage: "",
1065922
});
@@ -1089,7 +946,6 @@ async function tryHandleFollowUpReply(
1089946
const processing = await directStore.saveTaskArchiveResult({
1090947
inboxItemId: input.inboxItem.id,
1091948
confidence: 1,
1092-
plannerRun,
1093949
taskIds: selectedTasks.map((task) => task.id),
1094950
followUpMessage: "",
1095951
});
@@ -1119,7 +975,6 @@ async function tryHandleFollowUpReply(
1119975
const processing = await directStore.saveNeedsClarificationResult({
1120976
inboxItemId: input.inboxItem.id,
1121977
confidence: 1,
1122-
plannerRun,
1123978
reason: "Follow-up reply needs explicit reschedule or archive decision.",
1124979
followUpMessage:
1125980
"Noted. Tell me when you want to reschedule it, or say archive.",
@@ -1364,8 +1219,6 @@ function getCompatibilityTurnRouteFromPolicy(
13641219
return "conversation_then_mutation" as const;
13651220
case "execute_mutation":
13661221
return "mutation" as const;
1367-
case "recover_and_execute":
1368-
return "confirmed_mutation" as const;
13691222
}
13701223
}
13711224

@@ -1378,11 +1231,6 @@ function buildImmediateRouteFeedback(
13781231
text: "Checking your schedule",
13791232
chatAction: "typing",
13801233
};
1381-
case "recover_and_execute":
1382-
return {
1383-
text: "Applying that",
1384-
chatAction: "typing",
1385-
};
13861234
case "reply_only":
13871235
case "ask_clarification":
13881236
case "present_proposal":

0 commit comments

Comments
 (0)