remove rended driven email path

This commit is contained in:
Arjun 2026-05-18 17:52:15 +05:30
parent 3eaf752c94
commit 1e90ce1a49
9 changed files with 93 additions and 271 deletions

View file

@ -324,8 +324,7 @@ live:
Maintain a digest of email threads worth attention today, as a single \`emails\` block.
Without an event payload (cron / window / manual runs): scan \`gmail_sync/\` and emit the
full digest from scratch. In each email entry, store only \`threadId\` or \`threadUrl\`,
optional \`summary\`, and optional \`draft_response\`; the block hydrates Gmail metadata.
full digest from scratch.
With an event payload (event run): integrate the new thread into the existing digest
add it if new, update its entry if the threadId is already shown and don't re-list

View file

@ -14,7 +14,7 @@ const DAILY_NOTE_PATH = path.join(KNOWLEDGE_DIR, 'Today.md');
// on-disk `templateVersion` against this constant — if older or missing, the
// existing file is renamed to Today.md.bkp.<ISO-stamp> and replaced with the
// new template. v2 is the live-note rewrite (single objective, no `track:`).
const CANONICAL_DAILY_NOTE_VERSION = 3;
const CANONICAL_DAILY_NOTE_VERSION = 2;
const TODAY_LIVE_NOTE: z.infer<typeof LiveNoteSchema> = {
objective:
@ -24,7 +24,7 @@ const TODAY_LIVE_NOTE: z.infer<typeof LiveNoteSchema> = {
2. **Calendar** today's meetings as a single \`calendar\` block titled "Today's Meetings". Read \`calendar_sync/\` via \`workspace-readdir\`\`workspace-readFile\` each \`.json\`. Filter to today; after 10am drop meetings that have already ended. Always emit the block (use \`events: []\` when empty). Set \`showJoinButton: true\` if any event has a \`conferenceLink\`.
3. **Emails** a digest of email threads worth attention today, as a **single** fenced \`emails\` block (plural — never individual \`email\` blocks per thread). Body shape: \`{"title":"Today's Emails","emails":[...]}\`. Each entry should contain only \`threadId\` and/or \`threadUrl\`, plus optional \`summary\` if useful for why it matters. Do **not** copy \`subject\`, \`from\`, \`date\`, or \`latest_email\` from Gmail into the block — the email block hydrates the latest thread from Gmail directly. For threads needing a reply, add \`draft_response\` written in the user's voice — direct, informal, no fluff. For FYI threads, omit \`draft_response\`. Skip marketing, auto-notifications, and closed threads. Without an event payload, scan \`gmail_sync/\` (skip \`sync_state.json\` and \`attachments/\`), prioritising threads where frontmatter \`action = "reply"\` or \`"respond"\`, and use the filename/Thread ID to set \`threadId\`. With an event payload, integrate qualifying new threads into the existing digest (add a new entry for a new threadId; update the existing entry if shown). Don't re-list threads the user has already seen unless their state changed. If nothing qualifies: "No new emails."
3. **Emails** a digest of email threads worth attention today, as a **single** fenced \`emails\` block (plural — never individual \`email\` blocks per thread). Body shape: \`{"title":"Today's Emails","emails":[...]}\`. Each entry: \`threadId\`, \`subject\`, \`from\`, \`date\`, \`summary\`, \`latest_email\`. For threads needing a reply, add \`draft_response\` written in the user's voice — direct, informal, no fluff. For FYI threads, omit \`draft_response\`. Skip marketing, auto-notifications, and closed threads. Without an event payload, scan \`gmail_sync/\` (skip \`sync_state.json\` and \`attachments/\`), prioritising threads where frontmatter \`action = "reply"\` or \`"respond"\`. With an event payload, integrate qualifying new threads into the existing digest (add a new entry for a new threadId; update the existing entry if shown). Don't re-list threads the user has already seen unless their state changed. If nothing qualifies: "No new emails."
4. **What you missed** a short markdown summary of yesterday's meetings + emails that matter this morning. Pull decisions / action items from \`knowledge/Meetings/<source>/<yesterday>/\` (\`workspace-readdir\` recursive on \`knowledge/Meetings\`, filter folders matching yesterday's date, read each file). Skim \`gmail_sync/\` for unresolved threads. Skip recurring/routine events. If nothing notable: "Quiet day yesterday — nothing to flag."

View file

@ -163,11 +163,11 @@ If there are events, include them:
1. Use \`workspace-readdir\` with path \`gmail_sync\` to list files (skip \`sync_state.json\` and \`attachments/\`)
2. Use \`workspace-readFile\` to read the email markdown files (e.g. \`gmail_sync/threadid123.md\`)
3. Check the frontmatter \`action\` field — emails with \`action: reply\` or \`action: respond\` need a response
4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Each entry should contain only \`threadId\` and/or \`threadUrl\`, plus optional \`summary\` if useful. Do not copy sender, subject, date, or latest body into the block; the renderer hydrates those from Gmail. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. Example:
4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. Example:
\`\`\`
\\\`\\\`\\\`emails
{"title":"Today's Emails","emails":[{"threadId":"abc123","summary":"Payment confirmation needs acknowledgement.","draft_response":"Thanks for confirming. I'll update our records."},{"threadId":"def456","summary":"FYI security alert."}]}
{"title":"Today's Emails","emails":[{"threadId":"abc123","summary":"Payment confirmation","subject":"Google services payment","from":"Sender <sender@example.com>","date":"2026-04-01T11:28:39+05:30","latest_email":"Hi, I've made the payment...","draft_response":"Thanks for confirming. I'll update our records."},{"threadId":"def456","summary":"Security alert","subject":"New sign-in from Chrome","from":"Google <no-reply@accounts.google.com>","date":"2026-04-01T09:15:00+05:30","latest_email":"A new sign-in to your account was detected."}]}
\\\`\\\`\\\`
\`\`\`
@ -221,4 +221,4 @@ When you see a target region associated with your task (during a scheduled run),
- Use the existing content as context (e.g., to update rather than regenerate from scratch if appropriate)
- Do NOT include the target tags themselves in your response
`;
}
}

View file

@ -160,17 +160,19 @@ Required: \`events\` (array). Each event optionally has \`summary\`, \`start\`/\
## \`email\` — single email or thread digest (JSON)
Use for: surfacing one important Gmail thread. Prefer storing only a Gmail thread reference plus an optional draft reply; the renderer hydrates sender, subject, date, and latest body from Gmail.
Use for: surfacing one important thread latest message body, summary of prior context, optional draft reply.
\`\`\`email
{
"threadId": "18d5a3b2c1e4f567",
"summary": "Needs a reply on the Q3 launch blocker.",
"draft_response": "Thanks for flagging. I'll check with infra and get back to you today."
"subject": "Q3 launch readiness",
"from": "sarah@acme.com",
"date": "2026-04-19T16:42:00Z",
"summary": "Sarah confirms timeline; flagged blocker on infra capacity.",
"latest_email": "Hey — quick update on Q3...\\n\\nThanks,\\nSarah"
}
\`\`\`
Required: \`threadId\` or \`threadUrl\`. Optional: \`summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both"). Legacy fields \`subject\`, \`from\`, \`to\`, \`date\`, \`latest_email\`, and \`past_summary\` are supported for older notes, but don't emit them for Gmail threads.
Required: \`latest_email\` (string). Optional: \`threadId\`, \`summary\`, \`subject\`, \`from\`, \`to\`, \`date\`, \`past_summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both").
For digests of **many** threads, prefer a \`table\` (Subject | From | Snippet) — \`email\` is for one thread at a time.

View file

@ -134,7 +134,7 @@ async function publishCalendarSyncEvent(
// Configuration
const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
const LOOKBACK_DAYS = 7;
const REQUIRED_SCOPES = [
'https://www.googleapis.com/auth/calendar.events.readonly',

View file

@ -25,7 +25,7 @@ const CACHE_DIR = path.join(WorkDir, 'inbox_lists');
console.warn('[Gmail] Cache directory migration failed:', err);
}
})();
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
const REQUIRED_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly';
const MAX_THREADS_IN_DIGEST = 10;
const nhm = new NodeHtmlMarkdown();
@ -428,8 +428,8 @@ export async function listRecentThreadIds(daysAgo: number = 2): Promise<RecentTh
/**
* Build a GmailThreadSnapshot from an already-fetched threads.get response,
* classify it, and write to inbox_lists/. Shared by the renderer-driven
* fetchThreadSnapshot and the background sync (processThread).
* classify it, and write to inbox_lists/. Called by the background sync
* (processThread) the only path that materializes snapshots.
*
* Returns null when the thread has no visible (non-draft) messages
* those shouldn't show up in the inbox.
@ -444,6 +444,20 @@ async function buildAndCacheSnapshot(
if (!messages || messages.length === 0) return null;
const cached = readCachedSnapshot(threadId);
// Short-circuit: if the thread hasn't changed since we last classified it,
// skip the rebuild + classifier. Saves the cid-image fetches and one LLM
// call per unchanged thread (matters most during fullSync after a
// historyId expiry, where the whole window is re-walked).
// We require `importance` to be present too — pre-classifier cache files
// would otherwise stick around forever uncategorised.
if (
threadData.historyId &&
cached &&
cached.historyId === threadData.historyId &&
cached.snapshot.importance
) {
return cached.snapshot;
}
const heightCarryover = new Map<string, number>();
if (cached) {
for (const m of cached.snapshot.messages) {
@ -533,22 +547,6 @@ async function buildAndCacheSnapshot(
return snapshot;
}
export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?: string): Promise<GmailThreadSnapshot | null> {
const cached = readCachedSnapshot(threadId);
if (expectedHistoryId && cached && cached.historyId === expectedHistoryId) {
return cached.snapshot;
}
const auth = await GoogleClientFactory.getClient();
if (!auth) {
throw new Error('Gmail is not connected.');
}
const gmailClient = google.gmail({ version: 'v1', auth });
const res = await gmailClient.users.threads.get({ userId: 'me', id: threadId });
return buildAndCacheSnapshot(threadId, res.data, gmailClient, auth);
}
async function saveAttachment(gmail: gmail.Gmail, userId: string, msgId: string, part: gmail.Schema$MessagePart, attachmentsDir: string): Promise<string | null> {
const filename = part.filename;
const attId = part.body?.attachmentId;