Skip to content

Commit 444dc2c

Browse files
committed
Refine prompts and anchor scheduling to inbox time
1 parent 659aef5 commit 444dc2c

40 files changed

Lines changed: 1458 additions & 343 deletions

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Atlas
22

3-
Atlas is a Telegram-first brain-dump scheduler MVP. A user sends freeform text, the system turns it into structured tasks, places them onto a simple internal schedule, and follows up with Telegram reminders.
3+
Atlas is a chat-first brain-dump scheduler MVP. A user sends freeform text, the system turns it into structured tasks, places them onto a simple internal schedule, and follows up through its messaging bot surface.
44

55
## Repo shape
66

@@ -15,7 +15,10 @@ Atlas is a Telegram-first brain-dump scheduler MVP. A user sends freeform text,
1515

1616
- `pnpm dev`: run the Next.js app locally
1717
- `pnpm build`: build all workspaces
18+
- `pnpm eval:confirmed-mutation-recovery`: run the live OpenAI confirmed-mutation recovery eval fixture set against the current prompt
19+
- `pnpm eval:conversation-context`: run the live OpenAI conversation-context eval fixture set against the current prompt
1820
- `pnpm eval:planner`: run the live OpenAI planner eval fixture set against the current prompt
21+
- `pnpm eval:router-confirmation`: run the live OpenAI router-confirmation eval fixture set against the current prompt
1922
- `pnpm eval:turn-router`: run the live OpenAI turn-router eval fixture set against the current prompt
2023
- `pnpm lint`: lint all workspaces
2124
- `pnpm typecheck`: run TypeScript checks across the repo
@@ -87,8 +90,8 @@ This repo is designed for human-plus-agent collaboration.
8790

8891
## MVP flow
8992

90-
1. Telegram webhook receives a freeform message.
93+
1. The current messaging webhook receives a freeform message.
9194
2. If the sender is allowlisted but does not have an active Google Calendar connection, the app replies with a signed Google connect link and stops before ingress persistence.
9295
3. Linked users enter the normal flow: the app persists the inbox item, loads relevant task, schedule, and user-profile context, and sends a structured planning request through the OpenAI Responses API.
9396
4. Validated planning actions create or update tasks and schedule blocks through the repository layer.
94-
5. Telegram sends reminders tied to scheduled tasks.
97+
5. The messaging bot sends reminders tied to scheduled tasks.

apps/web/src/app/(admin)/inbox/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ export default function InboxPage() {
22
return (
33
<main style={{ padding: "2rem" }}>
44
<h1>Inbox Items</h1>
5-
<p>Inspect raw Telegram payloads, normalized text, and processing status.</p>
5+
<p>Inspect raw messaging payloads, normalized text, and processing status.</p>
66
</main>
77
);
88
}
9-

apps/web/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function HomePage() {
1313
<main style={{ padding: "3rem", maxWidth: "70rem", margin: "0 auto" }}>
1414
<h1 style={{ fontSize: "3rem", marginBottom: "1rem" }}>Atlas Admin</h1>
1515
<p style={{ maxWidth: "40rem", lineHeight: 1.6 }}>
16-
Telegram is the product surface. This interface exists for operators,
16+
The messaging bot is the product surface. This interface exists for operators,
1717
debugging, and schedule inspection.
1818
</p>
1919
<section

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

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe("process inbox item service", () => {
7979
expect(result.outcome).toBe("planned");
8080
expect(listPlannerRunsForTests()).toHaveLength(1);
8181
expect(listPlannerRunsForTests()[0]?.modelInput).toMatchObject({
82-
now: "2026-03-18T16:00:00.000Z"
82+
referenceTime: "2026-03-18T16:00:00.000Z"
8383
});
8484
expect(listScheduleBlocksForTests()).toHaveLength(1);
8585
expect(listTasksForTests()[0]).toMatchObject({
@@ -272,6 +272,62 @@ describe("process inbox item service", () => {
272272
expect("scheduleBlocks" in result ? result.scheduleBlocks[0]?.startAt : "").toContain("T22:00:00.000Z");
273273
});
274274

275+
it("schedules relative-minute requests from the inbox timestamp", async () => {
276+
seedInboxItemForProcessingTests({
277+
id: "inbox-relative-1",
278+
userId: "123",
279+
sourceEventId: "event-relative-1",
280+
rawText: "add schedule my car maintenance to my cal in like 15 min",
281+
normalizedText: "add schedule my car maintenance to my cal in like 15 min",
282+
processingStatus: "received",
283+
linkedTaskIds: [],
284+
createdAt: "2026-03-20T01:02:00.000Z"
285+
});
286+
287+
const result = await processInboxItem(
288+
{
289+
inboxItemId: "inbox-relative-1"
290+
},
291+
{
292+
calendar: getDefaultCalendarAdapter(),
293+
planner: async () => ({
294+
confidence: 0.9,
295+
summary: "Scheduled car maintenance in 15 minutes.",
296+
actions: [
297+
{
298+
type: "create_task",
299+
alias: "new_task_1",
300+
title: "Car maintenance",
301+
priority: "medium",
302+
urgency: "medium"
303+
},
304+
{
305+
type: "create_schedule_block",
306+
taskRef: {
307+
kind: "created_task",
308+
alias: "new_task_1"
309+
},
310+
scheduleConstraint: {
311+
dayReference: null,
312+
weekday: null,
313+
weekOffset: null,
314+
relativeMinutes: 15,
315+
explicitHour: null,
316+
minute: null,
317+
preferredWindow: null,
318+
sourceText: "in like 15 min"
319+
},
320+
reason: "The user asked for a relative start time."
321+
}
322+
]
323+
})
324+
}
325+
);
326+
327+
expect(result.outcome).toBe("planned");
328+
expect("scheduleBlocks" in result ? result.scheduleBlocks[0]?.startAt : "").toBe("2026-03-20T01:17:00.000Z");
329+
});
330+
275331
it("allows an app-owned planning text override for confirmed mutation recovery", async () => {
276332
const planner = vi.fn(async (): Promise<InboxPlanningOutput> => ({
277333
confidence: 0.9,
@@ -440,6 +496,58 @@ describe("process inbox item service", () => {
440496
expect("updatedBlock" in result ? result.updatedBlock.startAt : "").toBe("2026-03-21T17:00:00.000Z");
441497
});
442498

499+
it("uses the default open slot when the planner delegates slot choice", async () => {
500+
const store = getDefaultInboxProcessingStore();
501+
502+
seedInboxItemForProcessingTests({
503+
id: "inbox-pick-slot",
504+
userId: "123",
505+
sourceEventId: "event-pick-slot",
506+
rawText: "Schedule the oil change for me and just pick an opening",
507+
normalizedText: "Schedule the oil change for me and just pick an opening",
508+
processingStatus: "received",
509+
linkedTaskIds: [],
510+
createdAt: "2026-03-20T16:00:00.000Z"
511+
});
512+
513+
const result = await processInboxItem(
514+
{ inboxItemId: "inbox-pick-slot" },
515+
{
516+
store,
517+
calendar: getDefaultCalendarAdapter(),
518+
planner: async () => ({
519+
confidence: 0.88,
520+
summary: "Schedule the oil change using the next open slot.",
521+
actions: [
522+
{
523+
type: "create_task",
524+
alias: "new_task_1",
525+
title: "Oil change",
526+
priority: "medium",
527+
urgency: "medium"
528+
},
529+
{
530+
type: "create_schedule_block",
531+
taskRef: {
532+
kind: "created_task",
533+
alias: "new_task_1"
534+
},
535+
scheduleConstraint: null,
536+
reason: "The user delegated slot choice to Atlas."
537+
}
538+
]
539+
})
540+
}
541+
);
542+
543+
expect(result.outcome).toBe("planned");
544+
if (result.outcome !== "planned") {
545+
throw new Error("Expected a planned result.");
546+
}
547+
548+
expect(result.scheduleBlocks[0]?.startAt).toBe("2026-03-21T16:00:00.000Z");
549+
});
550+
443551
it("marks an existing task as done through the mutation path", async () => {
444552
const store = getDefaultInboxProcessingStore();
445553

@@ -631,7 +739,7 @@ describe("process inbox item service", () => {
631739
);
632740

633741
expect(result.outcome).toBe("updated_schedule");
634-
expect("updatedBlock" in result ? result.updatedBlock.startAt : "").toBe("2026-03-20T22:00:00.000Z");
742+
expect("updatedBlock" in result ? result.updatedBlock.startAt : "").toBe("2026-03-19T22:00:00.000Z");
635743
expect("updatedBlock" in result ? result.updatedBlock.id : "").toBe(listTasksForTests()[0]?.externalCalendarEventId);
636744
expect(listTasksForTests()[0]).toMatchObject({
637745
lastInboxItemId: "inbox-move",
@@ -806,7 +914,98 @@ describe("process inbox item service", () => {
806914
);
807915

808916
expect(result.outcome).toBe("updated_schedule");
809-
expect("updatedBlock" in result ? result.updatedBlock.startAt : "").toBe("2026-03-20T22:00:00.000Z");
917+
expect("updatedBlock" in result ? result.updatedBlock.startAt : "").toBe("2026-03-19T22:00:00.000Z");
918+
});
919+
920+
it("anchors busy-period lookup to the inbox reference time", async () => {
921+
const store = getDefaultInboxProcessingStore();
922+
const listBusyPeriods = vi.fn(async () => []);
923+
924+
await getDefaultGoogleCalendarConnectionStore().upsertConnection({
925+
userId: "123",
926+
providerAccountId: "google-user-1",
927+
email: "max@example.com",
928+
selectedCalendarId: "primary",
929+
selectedCalendarName: "Primary",
930+
accessToken: "access-token",
931+
refreshToken: "refresh-token",
932+
tokenExpiresAt: null,
933+
scopes: ["scope-a"],
934+
syncCursor: null,
935+
lastSyncedAt: null,
936+
revokedAt: null
937+
});
938+
939+
seedInboxItemForProcessingTests({
940+
id: "inbox-reference-busy",
941+
userId: "123",
942+
sourceEventId: "event-reference-busy",
943+
rawText: "Review launch checklist",
944+
normalizedText: "Review launch checklist",
945+
processingStatus: "received",
946+
linkedTaskIds: [],
947+
createdAt: "2026-03-19T16:00:00.000Z"
948+
});
949+
950+
await processInboxItem(
951+
{ inboxItemId: "inbox-reference-busy" },
952+
{
953+
store,
954+
calendar: {
955+
provider: "google-calendar",
956+
createEvent: async (input) => ({
957+
externalCalendarEventId: "event-1",
958+
externalCalendarId: input.externalCalendarId ?? "primary",
959+
scheduledStartAt: input.startAt,
960+
scheduledEndAt: input.endAt
961+
}),
962+
updateEvent: async (input) => ({
963+
externalCalendarEventId: input.externalCalendarEventId,
964+
externalCalendarId: input.externalCalendarId ?? "primary",
965+
scheduledStartAt: input.startAt,
966+
scheduledEndAt: input.endAt
967+
}),
968+
getEvent: async () => null,
969+
listBusyPeriods
970+
},
971+
planner: async () => ({
972+
confidence: 0.9,
973+
summary: "Captured and scheduled Review launch checklist.",
974+
actions: [
975+
{
976+
type: "create_task",
977+
alias: "new_task_1",
978+
title: "Review launch checklist",
979+
priority: "medium",
980+
urgency: "medium"
981+
},
982+
{
983+
type: "create_schedule_block",
984+
taskRef: {
985+
kind: "created_task",
986+
alias: "new_task_1"
987+
},
988+
scheduleConstraint: {
989+
dayReference: null,
990+
weekday: null,
991+
weekOffset: null,
992+
explicitHour: 9,
993+
minute: 0,
994+
preferredWindow: null,
995+
sourceText: "default next slot"
996+
},
997+
reason: "Schedule the new task in the next slot."
998+
}
999+
]
1000+
})
1001+
}
1002+
);
1003+
1004+
expect(listBusyPeriods).toHaveBeenCalledWith({
1005+
startAt: "2026-03-19T16:00:00.000Z",
1006+
endAt: "2026-04-02T16:00:00.000Z",
1007+
externalCalendarId: "primary"
1008+
});
8101009
});
8111010

8121011
it("marks invalid model output references for clarification", async () => {

0 commit comments

Comments
 (0)