Skip to content

Commit 659aef5

Browse files
authored
Improve duplicate-task clarification replies (#33)
1 parent c7ecd12 commit 659aef5

2 files changed

Lines changed: 508 additions & 6 deletions

File tree

apps/web/src/lib/server/process-inbox-item.test.ts

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,366 @@ describe("process inbox item service", () => {
956956
);
957957
});
958958

959+
it("returns a non-empty clarification reply when the planner explicitly asks to clarify", async () => {
960+
seedInboxItemForProcessingTests({
961+
id: "inbox-archive-clarify",
962+
userId: "123",
963+
sourceEventId: "event-archive-clarify",
964+
rawText: "archive that",
965+
normalizedText: "archive that",
966+
processingStatus: "received",
967+
linkedTaskIds: []
968+
});
969+
970+
const result = await processInboxItem(
971+
{ inboxItemId: "inbox-archive-clarify" },
972+
{
973+
calendar: getDefaultCalendarAdapter(),
974+
planner: async () => ({
975+
confidence: 0.32,
976+
summary: "Need clarification before archiving.",
977+
actions: [
978+
{
979+
type: "clarify",
980+
reason: "I couldn't safely apply that update. Tell me the exact task and what you'd like me to change."
981+
}
982+
]
983+
})
984+
}
985+
);
986+
987+
expect(result.outcome).toBe("needs_clarification");
988+
expect(result.followUpMessage).toBe(
989+
"I couldn't safely apply that update. Tell me the exact task and what you'd like me to change."
990+
);
991+
});
992+
993+
it("asks for clarification when multiple existing tasks share the same title", async () => {
994+
const store = getDefaultInboxProcessingStore();
995+
996+
seedInboxItemForProcessingTests({
997+
id: "inbox-duplicate-1",
998+
userId: "123",
999+
sourceEventId: "event-duplicate-1",
1000+
rawText: "Car maintenance",
1001+
normalizedText: "Car maintenance",
1002+
processingStatus: "received",
1003+
linkedTaskIds: [],
1004+
createdAt: "2026-03-20T16:00:00.000Z"
1005+
});
1006+
1007+
await processInboxItem(
1008+
{ inboxItemId: "inbox-duplicate-1" },
1009+
{
1010+
store,
1011+
calendar: getDefaultCalendarAdapter(),
1012+
planner: async () => ({
1013+
confidence: 0.9,
1014+
summary: "Scheduled Car maintenance.",
1015+
actions: [
1016+
{
1017+
type: "create_task",
1018+
alias: "new_task_1",
1019+
title: "Car maintenance",
1020+
priority: "medium",
1021+
urgency: "medium"
1022+
},
1023+
{
1024+
type: "create_schedule_block",
1025+
taskRef: {
1026+
kind: "created_task",
1027+
alias: "new_task_1"
1028+
},
1029+
scheduleConstraint: {
1030+
dayReference: null,
1031+
weekday: null,
1032+
weekOffset: null,
1033+
explicitHour: 9,
1034+
minute: 0,
1035+
preferredWindow: null,
1036+
sourceText: "default next slot"
1037+
},
1038+
reason: "Schedule the new task."
1039+
}
1040+
]
1041+
})
1042+
}
1043+
);
1044+
1045+
seedInboxItemForProcessingTests({
1046+
id: "inbox-duplicate-2",
1047+
userId: "123",
1048+
sourceEventId: "event-duplicate-2",
1049+
rawText: "Car maintenance",
1050+
normalizedText: "Car maintenance",
1051+
processingStatus: "received",
1052+
linkedTaskIds: [],
1053+
createdAt: "2026-03-20T17:00:00.000Z"
1054+
});
1055+
1056+
await processInboxItem(
1057+
{ inboxItemId: "inbox-duplicate-2" },
1058+
{
1059+
store,
1060+
calendar: getDefaultCalendarAdapter(),
1061+
planner: async () => ({
1062+
confidence: 0.9,
1063+
summary: "Scheduled Car maintenance.",
1064+
actions: [
1065+
{
1066+
type: "create_task",
1067+
alias: "new_task_1",
1068+
title: "Car maintenance",
1069+
priority: "medium",
1070+
urgency: "medium"
1071+
},
1072+
{
1073+
type: "create_schedule_block",
1074+
taskRef: {
1075+
kind: "created_task",
1076+
alias: "new_task_1"
1077+
},
1078+
scheduleConstraint: {
1079+
dayReference: null,
1080+
weekday: null,
1081+
weekOffset: null,
1082+
explicitHour: 10,
1083+
minute: 0,
1084+
preferredWindow: null,
1085+
sourceText: "default next slot"
1086+
},
1087+
reason: "Schedule the new task."
1088+
}
1089+
]
1090+
})
1091+
}
1092+
);
1093+
1094+
seedInboxItemForProcessingTests({
1095+
id: "inbox-duplicate-complete",
1096+
userId: "123",
1097+
sourceEventId: "event-duplicate-complete",
1098+
rawText: "car maintenance is done",
1099+
normalizedText: "car maintenance is done",
1100+
processingStatus: "received",
1101+
linkedTaskIds: [],
1102+
createdAt: "2026-03-20T18:00:00.000Z"
1103+
});
1104+
1105+
const result = await processInboxItem(
1106+
{
1107+
inboxItemId: "inbox-duplicate-complete",
1108+
planningInboxTextOverride: {
1109+
text: "Mark car maintenance as done."
1110+
}
1111+
},
1112+
{
1113+
store,
1114+
calendar: getDefaultCalendarAdapter(),
1115+
planner: async () => ({
1116+
confidence: 0.61,
1117+
summary: "Mark the existing car maintenance task as done.",
1118+
actions: [
1119+
{
1120+
type: "complete_task",
1121+
taskRef: {
1122+
kind: "existing_task",
1123+
alias: "existing_task_1"
1124+
},
1125+
reason: "The user said car maintenance is done."
1126+
}
1127+
]
1128+
})
1129+
}
1130+
);
1131+
1132+
expect(result.outcome).toBe("needs_clarification");
1133+
expect(result.followUpMessage).toContain("I found multiple tasks named 'Car maintenance'");
1134+
expect(result.followUpMessage).toContain("1. scheduled for");
1135+
expect(result.followUpMessage).toContain("2. scheduled for");
1136+
});
1137+
1138+
it("does not ask for clarification when duplicate titles only exist on closed tasks", async () => {
1139+
const store = getDefaultInboxProcessingStore();
1140+
1141+
seedInboxItemForProcessingTests({
1142+
id: "inbox-car-open",
1143+
userId: "123",
1144+
sourceEventId: "event-car-open",
1145+
rawText: "Car maintenance",
1146+
normalizedText: "Car maintenance",
1147+
processingStatus: "received",
1148+
linkedTaskIds: [],
1149+
createdAt: "2026-03-20T16:00:00.000Z"
1150+
});
1151+
1152+
await processInboxItem(
1153+
{ inboxItemId: "inbox-car-open" },
1154+
{
1155+
store,
1156+
calendar: getDefaultCalendarAdapter(),
1157+
planner: async () => ({
1158+
confidence: 0.9,
1159+
summary: "Scheduled Car maintenance.",
1160+
actions: [
1161+
{
1162+
type: "create_task",
1163+
alias: "new_task_1",
1164+
title: "Car maintenance",
1165+
priority: "medium",
1166+
urgency: "medium"
1167+
},
1168+
{
1169+
type: "create_schedule_block",
1170+
taskRef: {
1171+
kind: "created_task",
1172+
alias: "new_task_1"
1173+
},
1174+
scheduleConstraint: {
1175+
dayReference: null,
1176+
weekday: null,
1177+
weekOffset: null,
1178+
explicitHour: 9,
1179+
minute: 0,
1180+
preferredWindow: null,
1181+
sourceText: "default next slot"
1182+
},
1183+
reason: "Schedule the new task."
1184+
}
1185+
]
1186+
})
1187+
}
1188+
);
1189+
1190+
seedInboxItemForProcessingTests({
1191+
id: "inbox-car-closed",
1192+
userId: "123",
1193+
sourceEventId: "event-car-closed",
1194+
rawText: "Car maintenance is done",
1195+
normalizedText: "Car maintenance is done",
1196+
processingStatus: "received",
1197+
linkedTaskIds: [],
1198+
createdAt: "2026-03-20T17:00:00.000Z"
1199+
});
1200+
1201+
await processInboxItem(
1202+
{
1203+
inboxItemId: "inbox-car-closed",
1204+
planningInboxTextOverride: {
1205+
text: "Mark car maintenance as done."
1206+
}
1207+
},
1208+
{
1209+
store,
1210+
calendar: getDefaultCalendarAdapter(),
1211+
planner: async () => ({
1212+
confidence: 0.71,
1213+
summary: "Marked the remaining car maintenance task as done.",
1214+
actions: [
1215+
{
1216+
type: "complete_task",
1217+
taskRef: {
1218+
kind: "existing_task",
1219+
alias: "existing_task_1"
1220+
},
1221+
reason: "The user said car maintenance is done."
1222+
}
1223+
]
1224+
})
1225+
}
1226+
);
1227+
1228+
seedInboxItemForProcessingTests({
1229+
id: "inbox-car-second-open",
1230+
userId: "123",
1231+
sourceEventId: "event-car-second-open",
1232+
rawText: "Car maintenance",
1233+
normalizedText: "Car maintenance",
1234+
processingStatus: "received",
1235+
linkedTaskIds: [],
1236+
createdAt: "2026-03-20T18:00:00.000Z"
1237+
});
1238+
1239+
await processInboxItem(
1240+
{ inboxItemId: "inbox-car-second-open" },
1241+
{
1242+
store,
1243+
calendar: getDefaultCalendarAdapter(),
1244+
planner: async () => ({
1245+
confidence: 0.9,
1246+
summary: "Scheduled Car maintenance.",
1247+
actions: [
1248+
{
1249+
type: "create_task",
1250+
alias: "new_task_1",
1251+
title: "Car maintenance",
1252+
priority: "medium",
1253+
urgency: "medium"
1254+
},
1255+
{
1256+
type: "create_schedule_block",
1257+
taskRef: {
1258+
kind: "created_task",
1259+
alias: "new_task_1"
1260+
},
1261+
scheduleConstraint: {
1262+
dayReference: null,
1263+
weekday: null,
1264+
weekOffset: null,
1265+
explicitHour: 10,
1266+
minute: 0,
1267+
preferredWindow: null,
1268+
sourceText: "default next slot"
1269+
},
1270+
reason: "Schedule the new task."
1271+
}
1272+
]
1273+
})
1274+
}
1275+
);
1276+
1277+
seedInboxItemForProcessingTests({
1278+
id: "inbox-car-complete-open",
1279+
userId: "123",
1280+
sourceEventId: "event-car-complete-open",
1281+
rawText: "Car maintenance is done",
1282+
normalizedText: "Car maintenance is done",
1283+
processingStatus: "received",
1284+
linkedTaskIds: [],
1285+
createdAt: "2026-03-20T19:00:00.000Z"
1286+
});
1287+
1288+
const result = await processInboxItem(
1289+
{
1290+
inboxItemId: "inbox-car-complete-open",
1291+
planningInboxTextOverride: {
1292+
text: "Mark car maintenance as done."
1293+
}
1294+
},
1295+
{
1296+
store,
1297+
calendar: getDefaultCalendarAdapter(),
1298+
planner: async () => ({
1299+
confidence: 0.71,
1300+
summary: "Marked the remaining car maintenance task as done.",
1301+
actions: [
1302+
{
1303+
type: "complete_task",
1304+
taskRef: {
1305+
kind: "existing_task",
1306+
alias: "existing_task_2"
1307+
},
1308+
reason: "The user said car maintenance is done."
1309+
}
1310+
]
1311+
})
1312+
}
1313+
);
1314+
1315+
expect(result.outcome).toBe("completed_tasks");
1316+
expect(result.followUpMessage).toBe("Marked 'Car maintenance' as done.");
1317+
});
1318+
9591319
it("resets processing state and records a failed planner run when calendar creation fails", async () => {
9601320
const store = getDefaultInboxProcessingStore();
9611321

0 commit comments

Comments
 (0)