diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index a222f869..68cef1f2 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -7,6 +7,7 @@ import { init as initGmailSync } from "@x/core/dist/knowledge/sync_gmail.js"; import { init as initCalendarSync } from "@x/core/dist/knowledge/sync_calendar.js"; import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies.js"; import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js"; +import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -58,6 +59,9 @@ app.whenReady().then(() => { // start granola sync initGranolaSync(); + // start knowledge graph builder + initGraphBuilder(); + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/apps/x/packages/core/src/knowledge/build_graph.ts b/apps/x/packages/core/src/knowledge/build_graph.ts index a57fd6a7..8489f23d 100644 --- a/apps/x/packages/core/src/knowledge/build_graph.ts +++ b/apps/x/packages/core/src/knowledge/build_graph.ts @@ -20,6 +20,16 @@ import { const NOTES_OUTPUT_DIR = path.join(WorkDir, 'notes'); const NOTE_CREATION_AGENT = 'note_creation'; +// Configuration for the graph builder service +const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes (reduced frequency) +const SOURCE_FOLDERS = [ + 'gmail_sync', + 'fireflies_transcripts', + 'granola_notes' // Corrected from 'granola_meetings' +]; +const MAX_CONCURRENT_BATCHES = 1; // Process only 1 batch at a time to avoid overwhelming the agent +const BATCH_DELAY_MS = 5000; // 5 second delay between batches to avoid overwhelming the system + /** * Read content for specific files */ @@ -121,7 +131,7 @@ export async function buildGraph(sourceDir: string): Promise { return; } - const BATCH_SIZE = 25; // Process 25 files per agent run + const BATCH_SIZE = 10; // Reduced from 25 to 10 files per agent run for faster processing const totalBatches = Math.ceil(contentFiles.length / BATCH_SIZE); console.log(`Processing ${contentFiles.length} files in ${totalBatches} batches (${BATCH_SIZE} files per batch)...`); @@ -137,6 +147,12 @@ export async function buildGraph(sourceDir: string): Promise { await createNotesFromBatch(batch, batchNumber); console.log(`Batch ${batchNumber}/${totalBatches} complete`); + // Add delay between batches to avoid overwhelming the system + if (i + BATCH_SIZE < contentFiles.length) { + console.log(`Waiting ${BATCH_DELAY_MS/1000} seconds before next batch...`); + await new Promise(resolve => setTimeout(resolve, BATCH_DELAY_MS)); + } + // Mark files in this batch as processed for (const file of batch) { markFileAsProcessed(file.path, state); @@ -160,11 +176,66 @@ export async function buildGraph(sourceDir: string): Promise { } /** - * Main entry point - processes gmail_sync directory by default + * Process all configured source directories + */ +async function processAllSources(): Promise { + console.log('[GraphBuilder] Checking for new content in all sources...'); + + let anyFilesProcessed = false; + + for (const folder of SOURCE_FOLDERS) { + const sourceDir = path.join(WorkDir, folder); + + // Skip if folder doesn't exist + if (!fs.existsSync(sourceDir)) { + // Don't log this every time - it's noisy + continue; + } + + try { + // Quick check if there are any files to process before doing the full build + const state = loadState(); + const filesToProcess = getFilesToProcess(sourceDir, state); + + if (filesToProcess.length > 0) { + console.log(`[GraphBuilder] Found ${filesToProcess.length} new/changed files in ${folder}`); + await buildGraph(sourceDir); + anyFilesProcessed = true; + } + } catch (error) { + console.error(`[GraphBuilder] Error processing ${folder}:`, error); + // Continue with other folders even if one fails + } + } + + if (!anyFilesProcessed) { + console.log('[GraphBuilder] No new content to process'); + } else { + console.log('[GraphBuilder] Completed processing all sources'); + } +} + +/** + * Main entry point - runs as independent service monitoring all source folders */ export async function init() { - const defaultSourceDir = path.join(WorkDir, 'gmail_sync'); - await buildGraph(defaultSourceDir); + console.log('[GraphBuilder] Starting Knowledge Graph Builder Service...'); + console.log(`[GraphBuilder] Monitoring folders: ${SOURCE_FOLDERS.join(', ')}`); + console.log(`[GraphBuilder] Will check for new content every ${SYNC_INTERVAL_MS / 1000} seconds`); + + // Initial run + await processAllSources(); + + // Set up periodic processing + while (true) { + await new Promise(resolve => setTimeout(resolve, SYNC_INTERVAL_MS)); + + try { + await processAllSources(); + } catch (error) { + console.error('[GraphBuilder] Error in main loop:', error); + } + } } /** diff --git a/apps/x/packages/core/src/knowledge/granola/sync.ts b/apps/x/packages/core/src/knowledge/granola/sync.ts index 32c785a3..fcff60d7 100644 --- a/apps/x/packages/core/src/knowledge/granola/sync.ts +++ b/apps/x/packages/core/src/knowledge/granola/sync.ts @@ -2,7 +2,6 @@ import fs from 'fs'; import path from 'path'; import { homedir } from 'os'; import { WorkDir } from '../../config/config.js'; -import { buildGraph } from '../build_graph.js'; import container from '../../di/container.js'; import { IGranolaConfigRepo } from './repo.js'; import { @@ -314,12 +313,7 @@ async function syncNotes(): Promise { // Build knowledge graph if there were changes if (newCount > 0 || updatedCount > 0) { - console.log('[Granola] Starting knowledge graph build...'); - try { - await buildGraph(SYNC_DIR); - } catch (error) { - console.error('[Granola] Error building knowledge graph:', error); - } + // Graph building is now handled by the independent graph builder service } } diff --git a/apps/x/packages/core/src/knowledge/note_creation.md b/apps/x/packages/core/src/knowledge/note_creation.md index 59ea99cf..b9725b0c 100644 --- a/apps/x/packages/core/src/knowledge/note_creation.md +++ b/apps/x/packages/core/src/knowledge/note_creation.md @@ -19,14 +19,19 @@ tools: --- # Task -You are a note generation agent. Given a single source file (email, meeting transcript, or document), you will: +You are a memory agent. Given a single source file (email or meeting transcript), you will: -1. **Evaluate if the source is worth processing** -2. **Search for all existing related notes** -3. **Resolve entities to canonical names** -4. Identify new entities worth tracking -5. Extract structured information (decisions, commitments, key facts) -6. Create new notes or update existing notes in the Obsidian vault +1. **Determine source type (meeting or email)** +2. **Evaluate if the source is worth processing** +3. **Search for all existing related notes** +4. **Resolve entities to canonical names** +5. Identify new entities worth tracking (meetings only) +6. Extract structured information (decisions, commitments, key facts) +7. **Detect state changes (status updates, resolved items, role changes)** +8. Create new notes (meetings only) or update existing notes +9. **Apply state changes to existing notes** + +The core rule: **Meetings create notes. Emails enrich them.** You have full read access to the existing notes directory. Use this extensively to: - Find existing notes for people, organizations, projects mentioned @@ -34,15 +39,20 @@ You have full read access to the existing notes directory. Use this extensively - Understand existing relationships before updating - Avoid creating duplicate notes - Maintain consistency with existing content +- **Detect when new information changes the state of existing notes** # Inputs -1. **source_file**: Path to a single file to process (email, meeting transcript) +1. **source_file**: Path to a single file to process (email or meeting transcript) 2. **notes_folder**: Path to Obsidian vault (read/write access) +3. **user**: Information about the owner of this memory + - name: e.g., "Arj" + - email: e.g., "arj@rowboat.com" + - domain: e.g., "rowboat.com" # Tools Available -You have access to `executeCommand` to run shell commands. Use it for: +You have access to `executeCommand` to run shell commands: ``` executeCommand("ls {path}") # List directory contents executeCommand("cat {path}") # Read file contents @@ -67,22 +77,49 @@ Either: --- -# Step 0: Source Filtering +# The Core Rule: Meetings Create, Emails Enrich -Before processing, evaluate whether this source is worth tracking. +**Meetings create notes because:** +- You chose to spend time with these people +- If you met them, they matter enough to track +- Meeting transcripts have rich context -## Check for Prior Relationship +**Emails only update existing notes because:** +- Most emails are noise +- Without a meeting, there's no established relationship worth tracking +- Prevents memory bloat from random contacts -Before deciding to skip cold outreach, check if the sender exists in your notes: +**The only exception:** Warm intros from someone already in your memory. + +--- + +# Step 0: Determine Source Type + +Read the source file and determine if it's a meeting or email. ``` -executeCommand("grep -r -i -l '{sender name}' '{notes_folder}/'") -executeCommand("grep -r -i -l '{sender email}' '{notes_folder}/'") -executeCommand("grep -r -i -l '@{sender domain}' '{notes_folder}/'") +executeCommand("cat '{source_file}'") ``` -If any results, this is a known contact. Process the email. +**Meeting indicators:** +- Has `Attendees:` field +- Has `Meeting:` title +- Transcript format with speaker labels +- Calendar event metadata -## Skip These Sources +**Email indicators:** +- Has `From:` and `To:` fields +- Has `Subject:` field +- Email signature + +**Set processing mode:** +- `source_type = "meeting"` → Can create new notes +- `source_type = "email"` → Can only update existing notes + +--- + +# Step 1: Source Filtering + +## Skip These Sources (Both Meetings and Emails) ### Mass Emails and Newsletters @@ -97,19 +134,7 @@ If any results, this is a known contact. Process the email. **Action:** SKIP with reason "Newsletter/mass email" -### Cold Outreach (Unanswered) - -**Indicators:** -- First contact from unknown sender (grep returns no results) -- Sales/promotional pitch ("I'd love to show you", "Can I get 15 minutes") -- No reply in thread (Subject doesn't start with "Re:") -- Generic templates ("I noticed your company", "Congrats on the funding") - -**Action:** SKIP with reason "Cold outreach, no prior relationship" - -**Exception:** If you have replied (Subject starts with "Re:"), process it. - -### Automated/Transactional Emails +### Automated/Transactional **Indicators:** - From automated systems (notifications@, alerts@, no-reply@) @@ -120,7 +145,7 @@ If any results, this is a known contact. Process the email. **Action:** SKIP with reason "Automated/transactional" -### Low-Signal Emails +### Low-Signal **Indicators:** - Very short with no substance ("Thanks!", "Sounds good", "Got it") @@ -129,6 +154,37 @@ If any results, this is a known contact. Process the email. **Action:** SKIP with reason "Low signal" +## Email-Specific Filtering + +For emails, check if sender/recipients have existing notes: +```bash +executeCommand("grep -r -i -l '{sender email}' '{notes_folder}/'") +executeCommand("grep -r -i -l '{sender name}' '{notes_folder}/People/'") +``` + +**If no existing note found:** +- Check if this is a warm intro from someone in memory (see below) +- If not a warm intro → SKIP with reason "No existing relationship" + +**If existing note found:** +- Continue processing +- Will update existing note only + +### Detecting Warm Intros + +A warm intro is when someone already in your memory introduces you to someone new. + +**Indicators:** +- Subject contains "Intro:" or "Introduction:" +- Body contains "want to introduce" or "meet [Name]" +- Sender has an existing note in memory +- New person is CC'd or mentioned + +**If warm intro detected:** +- This is the ONE exception where email can create notes +- Create note for the introduced person +- Create org note for their company if needed + ## Filter Decision Output If skipping: @@ -137,25 +193,38 @@ SKIP Reason: {reason} ``` -If processing, continue to Step 1. +If processing, continue to Step 2. --- -# Step 1: Read and Parse Source File +# Step 2: Read and Parse Source File ``` executeCommand("cat '{source_file}'") ``` Extract metadata: -- **Date:** From `Date:` header, or parse from filename `YYYY-MM-DD-*.md` -- **Type:** `email` (has From:/To:), `meeting` (has Attendees: or transcript format) -- **Title:** From `Subject:` or `Meeting:` header, or filename -- **From:** Sender email/name -- **To:** Recipient(s) -- **People mentioned:** Names in body -- **Organizations mentioned:** Company names in body -## 1b: Extract All Name Variants +**For meetings:** +- **Date:** From header or filename +- **Title:** Meeting name +- **Attendees:** List of participants +- **Duration:** If available + +**For emails:** +- **Date:** From `Date:` header +- **Subject:** From `Subject:` header +- **From:** Sender email/name +- **To/Cc:** Recipients + +## 2a: Exclude Self + +Never create or update notes for: +- The user (matches user.name, user.email, or @user.domain) +- Anyone @{user.domain} (colleagues at user's company) + +Filter these out from attendees/participants before proceeding. + +## 2b: Extract All Name Variants From the source, collect every way entities are referenced: @@ -190,11 +259,11 @@ Variants found: --- -# Step 2: Search for Existing Notes +# Step 3: Search for Existing Notes For each variant identified, search the notes folder thoroughly. -## 2a: Search by People +## 3a: Search by People ```bash # Search by full name executeCommand("grep -r -i -l 'Sarah Chen' '{notes_folder}/'") @@ -212,7 +281,7 @@ executeCommand("grep -r -i -l '@acme.com' '{notes_folder}/'") executeCommand("grep -r -i 'Aliases.*Sarah' '{notes_folder}/People/'") ``` -## 2b: Search by Organizations +## 3b: Search by Organizations ```bash # List all organization notes executeCommand("ls '{notes_folder}/Organizations/'") @@ -227,7 +296,7 @@ executeCommand("grep -r -i 'Domain.*acme.com' '{notes_folder}/Organizations/'") executeCommand("grep -r -i 'Aliases.*Acme' '{notes_folder}/Organizations/'") ``` -## 2c: Search by Projects and Topics +## 3c: Search by Projects and Topics ```bash # List all projects executeCommand("ls '{notes_folder}/Projects/'") @@ -244,7 +313,7 @@ executeCommand("ls '{notes_folder}/Topics/'") executeCommand("grep -r -i 'SOC 2' '{notes_folder}/Topics/'") ``` -## 2d: Read Candidate Notes +## 3d: Read Candidate Notes For every note file found in searches, read it to understand context: ```bash @@ -261,8 +330,10 @@ executeCommand("cat '{notes_folder}/Projects/Acme Integration.md'") - See organization context for disambiguation - Check what's already captured (avoid duplicates) - Review open items (some might be resolved) +- **Check current status fields (might need updating)** +- **Check current roles (might have changed)** -## 2e: Matching Criteria +## 3e: Matching Criteria Use these criteria to determine if a variant matches an existing note: @@ -296,11 +367,11 @@ Use these criteria to determine if a variant matches an existing note: --- -# Step 3: Resolve Entities to Canonical Names +# Step 4: Resolve Entities to Canonical Names -Using the search results from Step 2, resolve each variant to a canonical name. +Using the search results from Step 3, resolve each variant to a canonical name. -## 3a: Build Resolution Map +## 4a: Build Resolution Map Create a mapping from every source reference to its canonical form: ``` @@ -317,7 +388,18 @@ Resolution Map: - "the integration" → "Acme Integration" (same project) ``` -## 3b: Disambiguation Rules +## 4b: Apply Source Type Rules + +**If source_type == "meeting":** +- Resolved entities → Update existing notes +- New entities that pass filters → Create new notes + +**If source_type == "email":** +- Resolved entities → Update existing notes +- New entities → Do NOT create notes (skip them) +- Exception: Warm intro → Create note for introduced person + +## 4c: Disambiguation Rules When multiple candidates match a variant, disambiguate: @@ -351,19 +433,9 @@ If still ambiguous, prefer the person with more recent activity in notes. **If still ambiguous:** - Flag in resolution map: "David" → "David (ambiguous - could be David Kim or David Chen)" -- Will handle in Step 4 +- Will handle in Step 5 -## 3c: Handle Unresolved Entities - -For entities with no match found: -``` -Unresolved: -- "Jennifer" (CTO at Acme) → No existing note, NEW ENTITY -- "SOC 2 compliance" → No existing topic, NEW ENTITY -- "Mike" (no org context) → Cannot resolve, SKIP or mention in activity only -``` - -## 3d: Resolution Map Output +## 4d: Resolution Map Output Final resolution map before proceeding: ``` @@ -373,10 +445,13 @@ RESOLVED (use canonical name): - "Acme", "Acme Corp", "@acme.com" → [[Acme Corp]] - "the pilot", "the integration" → [[Acme Integration]] -NEW ENTITIES (create notes): +NEW ENTITIES (meetings only — create notes): - "Jennifer" (CTO, Acme Corp) → Create [[Jennifer]] or [[Jennifer (Acme Corp)]] - "SOC 2" → Create [[Security Compliance]] +NEW ENTITIES (emails — do not create): +- "Random Person" → Skip, no existing relationship + AMBIGUOUS (flag or skip): - "Mike" (no context) → Mention in activity only, don't create note @@ -386,15 +461,18 @@ SKIP (doesn't warrant note): --- -# Step 4: Identify New Entities +# Step 5: Identify New Entities (Meetings Only) + +**This step only applies to meetings. For emails, skip to Step 6.** For entities not resolved to existing notes, determine if they warrant new notes. -## People +## People (Meetings Only) ### Who Gets a Note -**CREATE a note for people who are:** +**CREATE a note for meeting attendees who are:** +- External (not @user.domain) - Decision makers or key contacts at customers, prospects, or partners - Investors or potential investors - Candidates you are interviewing @@ -405,7 +483,8 @@ For entities not resolved to existing notes, determine if they warrant new notes **DO NOT create notes for:** - Transactional service providers (bank employees, support reps) - One-time administrative contacts -- Large CC list participants not directly involved +- Large group meeting attendees you didn't interact with +- Internal colleagues (@user.domain) - Assistants handling only logistics - Generic role-based contacts @@ -418,6 +497,37 @@ Ask: If I had a call with this person next week, would I want notes beforehand? - Investor you're pitching → **Yes, create note** - Recruiter scheduling interviews → **No, skip** +### Role Inference + +If role is not explicitly stated, infer from context: + +**From email signatures:** +- Often contains title + +**From meeting context:** +- Organizer of cross-company meeting → likely senior or partnerships +- Technical questions → likely engineering +- Pricing questions → likely procurement or finance +- Product feedback → likely product + +**From email patterns:** +- firstname@company.com → often founder or senior +- firstname.lastname@company.com → often larger company employee + +**From conversation content:** +- "I'll need to check with my team" → manager +- "Let me run this by leadership" → IC or mid-level +- "I can make that call" → decision maker + +**Format in note:** +```markdown +**Role:** Product Lead (inferred from evaluation discussions) +**Role:** Senior (inferred — organized cross-company meeting) +**Role:** Engineering (inferred — asked technical integration questions) +``` + +**Never write just "Unknown" if you can make a reasonable inference.** + ### Relationship Type Guide | Relationship Type | Create People Notes? | Create Org Note? | @@ -442,41 +552,34 @@ For people who don't warrant their own note, add to Organization note's Contacts - Sarah Lee — Support, handled wire transfer issue ``` -## Organizations +## Organizations (Meetings Only) -**CREATE a note for:** -- Customers and prospects -- Investors and funds -- Strategic partners -- Key vendors -- Competitors worth tracking +**CREATE a note if:** +- Someone from that org attended the meeting +- It's a customer, prospect, investor, or partner -**DO NOT create notes for:** -- One-time service providers -- Utilities and commodity services -- Tools mentioned in passing (Zoom, Slack) +**DO NOT create for:** +- Tool/service providers mentioned in passing +- One-time transactional vendors -## Projects +## Projects (Meetings Only) -**CREATE a note for:** -- Deals in progress -- Product initiatives -- Hiring for specific roles -- Fundraising rounds -- Partnerships being negotiated +**CREATE a note if:** +- Discussed substantively in the meeting +- Has a goal and timeline +- Involves multiple interactions -## Topics +## Topics (Meetings Only) -**CREATE a note for:** -- Recurring themes across conversations -- Ongoing discussion areas (security compliance, pricing strategy) -- Decision areas that span multiple projects +**CREATE a note if:** +- Recurring theme discussed +- Will come up again across conversations --- -# Step 5: Extract Content +# Step 6: Extract Content -For each entity (resolved or new) that will have a note, extract relevant content. +For each entity that has or will have a note, extract relevant content. ## Decisions @@ -498,37 +601,248 @@ For each entity (resolved or new) that will have a note, extract relevant conten ## Key Facts -**Extract if:** -- Specific numbers (budget, timeline, team size) -- Preferences or working style -- Background information -- Authority or decision process -- Concerns or constraints +Key facts should be **substantive information about the entity** — not commentary about missing data. -**Skip if:** -- Generic sentiment -- Obvious from role -- Already captured in existing note (check from Step 2) +**Extract if:** +- Specific numbers (budget: $50K, team size: 12, timeline: Q2) +- Preferences or working style ("prefers async communication") +- Background information ("previously at Google") +- Authority or decision process ("needs CEO sign-off") +- Concerns or constraints ("security is top priority") +- What they're evaluating or interested in +- What was discussed or proposed +- Technical requirements or specifications + +**Never include:** +- Meta-commentary about missing data ("Name only provided", "Role not mentioned") +- Obvious facts ("Works at Acme" — that's in the Info section) +- Placeholder text ("Unknown", "TBD") +- Data quality observations ("Full name not in email") + +**If there are no substantive key facts, leave the section empty.** An empty section is better than filler. + +**Good key facts:** +```markdown +## Key facts +- Evaluating AI copilot for in-app experience +- Three use cases discussed: pre-purchase sales, onboarding, coaching +- Budget approved for Q2 pilot +- Needs SOC 2 compliance before proceeding +``` + +**Bad key facts:** +```markdown +## Key facts +- Name only provided; full name/role not in email. +- Email address not available. +- Meeting was 50 minutes. +``` + +## Open Items + +Open items are **commitments and next steps from the conversation** — not tasks to fill in missing data. + +**Include:** +- Commitments made: "I'll send the documentation by Friday" +- Requests received: "Can you share pricing?" +- Next steps discussed: "Let's schedule a technical deep-dive" +- Follow-ups agreed: "Will loop in their CTO" + +**Format:** +```markdown +- [ ] {Action} — {owner if not you}, {due date if known} +``` + +**Never include:** +- Data gaps: "Find their full name", "Get their email", "Add role" +- Wishes: "Would be good to know their budget" +- Agent tasks: "Research their company" + +**If there are no actual commitments or next steps, leave the section empty.** + +**Good open items:** +```markdown +## Open items +- [ ] Send API documentation — by Friday +- [ ] Schedule follow-up call with CTO +- [ ] Share pricing proposal — after technical review +``` + +**Bad open items:** +```markdown +## Open items +- [ ] Find Matteo's full name, role, and email at [[Eight Sleep]] +- [ ] Add Anurag's role/title at Groww +- [ ] Research Eight Sleep company background +``` + +## Summary + +The summary should answer: **"Who is this person and why do I know them?"** + +**Write 2-3 sentences covering:** +- Their role/function (even if inferred) +- The context of your relationship +- What you're discussing or working on together + +**Focus on the relationship, not the communication method.** + +**Good summaries:** +```markdown +## Summary +Product contact at [[Eight Sleep]] exploring an AI copilot for their app. +Initial discussions covered sales assistance, onboarding, and coaching use cases. +Currently evaluating fit with their product roadmap. +``` +```markdown +## Summary +VP Engineering at [[Acme Corp]] leading their integration project. +Key technical decision-maker. Working toward Q2 pilot launch. +``` + +**Bad summaries:** +```markdown +## Summary +Contact at [[Eight Sleep]]; received an outbound pitch from [[Arjun Maheswaran]] +about an in-app AI copilot concept. +``` +```markdown +## Summary +Attendee on the scheduled "Groww <> RowBoat" meeting (Aug 12, 2024). +``` + +**Why these are bad:** +- "Received an outbound pitch" — describes the email, not the relationship +- "Attendee on scheduled meeting" — describes attendance, not who they are + +**Infer when needed:** +If role is unknown but context suggests it, say so: +- "Likely product or partnerships (evaluating AI integration)" +- "Senior contact (organized cross-company meeting)" ## Activity Summary One line summarizing this source's relevance to the entity: ``` -**{YYYY-MM-DD}** ({email|meeting}): {Summary with [[links]]} +**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]} ``` **Important:** Use canonical names from resolution map in all summaries: ``` # Correct (uses canonical names): -**2025-01-15** (email): [[Sarah Chen]] confirmed timeline with [[David Kim]]. Blocked on [[Security Compliance]]. +**2025-01-15** (meeting): [[Sarah Chen]] confirmed timeline with [[David Kim]]. Blocked on [[Security Compliance]]. # Incorrect (uses variants): -**2025-01-15** (email): Sarah confirmed timeline with David. Blocked on SOC 2. +**2025-01-15** (meeting): Sarah confirmed timeline with David. Blocked on SOC 2. ``` --- -# Step 6: Check for Duplicates and Conflicts +# Step 7: Detect State Changes + +Review the extracted content for signals that existing note fields should be updated. + +## 7a: Project Status Changes + +**Look for these signals:** + +| Signal | New Status | +|--------|------------| +| "Moving forward" / "approved" / "signed" / "green light" | active | +| "On hold" / "pausing" / "delayed" / "pushed back" | on hold | +| "Cancelled" / "not proceeding" / "killed" / "passed" | cancelled | +| "Launched" / "completed" / "done" / "shipped" | completed | +| "Exploring" / "considering" / "evaluating" / "might" | planning | + +**Action:** If a related project note exists and the signal is clear, update the `**Status:**` field. + +**Example:** +``` +Source: "Great news — leadership approved the pilot!" +Current: **Status:** planning +Update to: **Status:** active +``` + +**Be conservative:** Only update status when the signal is unambiguous. If unclear, add to activity log but don't change status. + +## 7b: Open Item Resolution + +**Look for signals that a previously tracked open item is now complete:** + +| Signal | Action | +|--------|--------| +| "Here's the [X] you requested" | Mark [X] complete | +| "I've sent the [X]" | Mark [X] complete | +| "The [X] is ready" | Mark [X] complete | +| "[X] is done" | Mark [X] complete | +| "Attached is the [X]" | Mark [X] complete | + +**How to match:** +1. Read existing open items from the note +2. Look for items that match what was delivered/completed +3. Change `- [ ]` to `- [x]` with completion date + +**Example:** +``` +Source: "Here's the API documentation you requested." +Current: - [ ] Send API documentation — by Friday +Update to: - [x] Send API documentation — completed 2025-01-16 +``` + +**Be conservative:** Only mark complete if there's a clear match. If unsure, add to activity log but don't mark complete. + +## 7c: Role/Title Changes + +**Look for signals:** +- New title in email signature +- "I've been promoted to..." +- "I'm now the..." +- "I've moved to the [X] team" +- Different role mentioned than what's in the note + +**Action:** Update the `**Role:**` field in person note. + +**Example:** +``` +Source: Email signature shows "VP Engineering" +Current: **Role:** Engineering Lead +Update to: **Role:** VP Engineering (updated 2025-01-16) +``` + +## 7d: Organization/Relationship Changes + +**Look for signals:** +- "I've joined [New Company]" +- "We're now a customer" / "We signed the contract" +- "We've partnered with..." +- "They acquired us" +- New email domain for known person + +**Action:** Update relevant fields: +- Person's `**Organization:**` field +- Org's `**Relationship:**` field (prospect → customer, etc.) + +**Example:** +``` +Source: "Excited to announce we've signed the contract!" +Current: **Relationship:** prospect +Update to: **Relationship:** customer +``` + +## 7e: Build State Change List + +Before writing, compile all detected state changes: +``` +STATE CHANGES: +- [[Acme Integration]]: Status planning → active (leadership approved) +- [[Sarah Chen]]: Role "Engineering Lead" → "VP Engineering" (signature) +- [[Sarah Chen]]: Open item "Send API documentation" → completed +- [[Acme Corp]]: Relationship prospect → customer (contract signed) +``` + +--- + +# Step 8: Check for Duplicates and Conflicts Before writing, compare extracted content against existing notes. @@ -545,7 +859,9 @@ Review key facts against existing. Skip duplicates. ## Check Open Items -Some open items might be resolved by this source. Mark with [x] when updating. +Review open items for: +- Duplicates (don't add same item twice) +- Items that should be marked complete (from Step 7b) ## Check for Conflicts @@ -556,37 +872,97 @@ If new info contradicts existing: --- -# Step 7: Write Updates +# Step 9: Write Updates -## 7a: Update Existing Notes +## 9a: Meetings — Create and Update Notes -Read current content first: +**For new entities (meetings only):** ```bash -executeCommand("cat '{notes_folder}/People/Sarah Chen.md'") +executeCommand("write '{notes_folder}/People/Jennifer.md' '{content}'") ``` -Apply updates: -- Append new activity entry at TOP of Activity section (reverse chronological) +**For existing entities:** +- Read current content first +- Add activity entry at TOP of Activity section (reverse chronological) - Update "Last seen" date - Add new key facts (skip duplicates) - Add new open items - Add new decisions - Add new relationships - Update summary ONLY if significant new understanding - -Write complete updated note: ```bash +executeCommand("cat '{notes_folder}/People/Sarah Chen.md'") +# ... modify content ... executeCommand("write '{notes_folder}/People/Sarah Chen.md' '{full_updated_content}'") ``` -## 7b: Create New Notes +## 9b: Emails — Update Existing Notes Only -Use templates below. Write complete note: +**Only update notes that already exist.** + +Do NOT create new notes from emails (except warm intros). + +For existing notes: +- Add activity entry +- Update "Last seen" date +- Add new key facts +- Add new commitments +- Update open items if resolved + +## 9c: Apply State Changes + +For each state change identified in Step 7: + +### Update Project Status ```bash -executeCommand("write '{notes_folder}/People/Jennifer.md' '{note_content}'") +# Read current project note +executeCommand("cat '{notes_folder}/Projects/Acme Integration.md'") + +# Update the Status field +# Change: **Status:** planning +# To: **Status:** active ``` -## 7c: Update Aliases +### Mark Open Items Complete +```bash +# Read current note +executeCommand("cat '{notes_folder}/People/Sarah Chen.md'") + +# Find matching open item and update +# Change: - [ ] Send API documentation — by Friday +# To: - [x] Send API documentation — completed 2025-01-16 +``` + +### Update Role +```bash +# Read current person note +executeCommand("cat '{notes_folder}/People/Sarah Chen.md'") + +# Update role field +# Change: **Role:** Engineering Lead +# To: **Role:** VP Engineering +``` + +### Update Relationship +```bash +# Read current org note +executeCommand("cat '{notes_folder}/Organizations/Acme Corp.md'") + +# Update relationship field +# Change: **Relationship:** prospect +# To: **Relationship:** customer +``` + +### Log State Changes in Activity + +When applying a state change, also note it in the activity log: +```markdown +- **2025-01-16** (email): Leadership approved pilot. [Status → active] Contract being drafted. +``` + +Use `[Field → new value]` notation to make state changes visible in the activity log. + +## 9d: Update Aliases If you discovered new name variants during resolution, add them to Aliases field: ```markdown @@ -599,16 +975,17 @@ If you discovered new name variants during resolution, add them to Aliases field **Aliases:** Sarah, S. Chen, Sarah C. ``` -## 7d: Writing Rules +## 9e: Writing Rules - **Always use canonical names** from resolution map for all `[[links]]` - Use YYYY-MM-DD format for dates - Be concise: one line per activity entry +- Note state changes with `[Field → value]` in activity - Escape quotes properly in shell commands --- -# Step 8: Ensure Bidirectional Links +# Step 10: Ensure Bidirectional Links After writing, verify links go both ways. @@ -640,15 +1017,15 @@ If not found, update Jennifer.md to add the link. # {Full Name} ## Info -**Role:** {role or "Unknown"} -**Organization:** [[{organization}]] or "Unknown" -**Email:** {email or "Unknown"} +**Role:** {role, or inferred role with qualifier, or leave blank if truly unknown} +**Organization:** [[{organization}]] or leave blank +**Email:** {email or leave blank} **Aliases:** {comma-separated: first name, nicknames, email} -**First seen:** {YYYY-MM-DD} +**First met:** {YYYY-MM-DD} **Last seen:** {YYYY-MM-DD} ## Summary -{2-3 sentences: Who they are, how you know them, what the relationship is about.} +{2-3 sentences: Who they are, why you know them, what you're working on together. Focus on relationship and context, not communication method.} ## Connected to - [[{Organization}]] — works at @@ -656,13 +1033,14 @@ If not found, update Jennifer.md to add the link. - [[{Project}]] — {role} ## Activity -- **{YYYY-MM-DD}** ({type}): {Summary with [[links]]} +- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]} {[State changes if any]} ## Key facts -- {Fact} +{Substantive facts only. Leave empty if none. Never include data gap commentary.} ## Open items -- [ ] {Action} — {owner if not you}, {due date if known} +{Commitments and next steps only. Leave empty if none. Never include "find their email" type items.} +{Mark completed items with [x] and completion date.} ``` ## Organizations @@ -671,15 +1049,15 @@ If not found, update Jennifer.md to add the link. ## Info **Type:** {company|team|institution|other} -**Industry:** {industry or "Unknown"} +**Industry:** {industry or leave blank} **Relationship:** {customer|prospect|partner|competitor|vendor|other} **Domain:** {primary email domain} **Aliases:** {comma-separated: short names, abbreviations} -**First seen:** {YYYY-MM-DD} +**First met:** {YYYY-MM-DD} **Last seen:** {YYYY-MM-DD} ## Summary -{2-3 sentences: What this org is, what your relationship is.} +{2-3 sentences: What this org is, what your relationship is, what you're working on together.} ## People - [[{Person}]] — {role} @@ -692,13 +1070,13 @@ If not found, update Jennifer.md to add the link. - [[{Project}]] — {relationship} ## Activity -- **{YYYY-MM-DD}** ({type}): {Summary} +- **{YYYY-MM-DD}** ({meeting|email}): {Summary} {[State changes if any]} ## Key facts -- {Fact} +{Substantive facts only. Leave empty if none.} ## Open items -- [ ] {Item} +{Commitments and next steps only. Leave empty if none.} ``` ## Projects @@ -708,7 +1086,7 @@ If not found, update Jennifer.md to add the link. ## Info **Type:** {deal|product|initiative|hiring|other} **Status:** {active|planning|on hold|completed|cancelled} -**Started:** {YYYY-MM-DD or "Unknown"} +**Started:** {YYYY-MM-DD or leave blank} **Last activity:** {YYYY-MM-DD} ## Summary @@ -724,17 +1102,17 @@ If not found, update Jennifer.md to add the link. - [[{Topic or Project}]] — {relationship} ## Timeline -**{YYYY-MM-DD}** ({source type}) -{What happened. Key points.} +**{YYYY-MM-DD}** ({meeting|email}) +{What happened. Key points.} {[Status → new status] if changed} ## Decisions - **{YYYY-MM-DD}**: {Decision}. {Rationale}. ## Open items -- [ ] {Item} +{Commitments and next steps only. Leave empty if none.} ## Key facts -- {Fact} +{Substantive facts only. Leave empty if none.} ``` ## Topics @@ -753,17 +1131,17 @@ If not found, update Jennifer.md to add the link. - [[{Entity}]] — {relationship} ## Log -**{YYYY-MM-DD}** ({source}: {title}) +**{YYYY-MM-DD}** ({meeting|email}: {title}) {Summary with [[links]]} ## Decisions - **{YYYY-MM-DD}**: {Decision} ## Open items -- [ ] {Item} +{Commitments and next steps only. Leave empty if none.} ## Key facts -- {Fact} +{Substantive facts only. Leave empty if none.} ``` --- @@ -777,8 +1155,9 @@ If not found, update Jennifer.md to add the link. 3. Read candidate notes, check org/role/email context 4. Disambiguate: org context > email match > role match > recency 5. Build resolution map -6. Use canonical names in ALL output -7. Update Aliases with newly discovered variants +6. Apply source type rules (meetings create, emails only update) +7. Use canonical names in ALL output +8. Update Aliases with newly discovered variants ## Common Patterns @@ -801,327 +1180,442 @@ If not found, update Jennifer.md to add the link. ## Handling Failures -| Situation | Action | -|-----------|--------| -| No match + passes "Would I prep?" test | Create new note | -| No match + fails "Would I prep?" test | Mention in org/project note only | -| Multiple matches + can disambiguate | Use disambiguation rules | -| Multiple matches + cannot disambiguate | Create note with "(possibly same as [[X]])" | -| Conflicting information | Note both versions, flag for review | +| Situation | Source Type | Action | +|-----------|-------------|--------| +| No match + passes "Would I prep?" | Meeting | Create new note | +| No match + passes "Would I prep?" | Email | Do NOT create (skip) | +| No match + fails "Would I prep?" | Both | Mention in org note only | +| Multiple matches + can disambiguate | Both | Use disambiguation rules | +| Multiple matches + cannot disambiguate | Meeting | Create note with "(possibly same as [[X]])" | +| Multiple matches + cannot disambiguate | Email | Skip, don't update either | +| Conflicting information | Both | Note both versions, flag for review | --- # Examples -## Example 1: Full Resolution Flow +## Example 1: Meeting — Creates Notes -**source_file:** `2025-01-15-email.md` +**source_file:** `2025-01-15-meeting.md` +``` +Meeting: Acme Integration Kickoff +Date: 2025-01-15 +Attendees: Sarah Chen (sarah@acme.com), David Kim (david@acme.com), Arj (arj@rowboat.com) + +Transcript: +Sarah: Thanks for meeting. We're excited about the pilot. +David: From a technical side, we need API access first. +Sarah: Our CTO Jennifer wants to join the next call. +... +``` + +### Step 0: Determine Source Type + +Has `Meeting:` and `Attendees:` → `source_type = "meeting"` → Can create notes + +### Step 1: Filter + +Not mass email, not automated. Continue. + +### Step 2: Parse + +- Date: 2025-01-15 +- Attendees: Sarah Chen, David Kim, Arj (self — exclude) +- Variants: "Sarah Chen", "sarah@acme.com", "David Kim", "David", "Jennifer", "CTO", "Acme", "the pilot" + +### Step 3: Search Existing Notes +```bash +executeCommand("grep -r -i -l 'Sarah Chen' 'notes/'") +# Output: (none) + +executeCommand("grep -r -i -l 'acme' 'notes/'") +# Output: (none) +``` + +No existing notes. This is a new relationship. + +### Step 4: Resolve Entities + +**Resolution Map:** +``` +NEW ENTITIES (meeting — create): +- "Sarah Chen" → Create [[Sarah Chen]] +- "David Kim" → Create [[David Kim]] +- "Jennifer" (CTO) → Create [[Jennifer]] +- "Acme" → Create [[Acme Corp]] +- "the pilot" → Create [[Acme Integration]] +``` + +### Step 5: Identify New Entities + +All attendees are external and pass "Would I prep?" test: +- Sarah Chen (key contact) → Create +- David Kim (technical contact) → Create +- Jennifer (CTO, mentioned) → Create +- Acme Corp (prospect company) → Create +- Acme Integration (project) → Create + +### Step 6: Extract Content + +- Decisions: None yet +- Commitments: Provide API access, schedule call with Jennifer +- Key facts: Excited about pilot, need API access first, CTO involved + +### Step 7: Detect State Changes + +No existing notes → No state changes to detect. + +### Steps 8-10: Check, Write, Link + +Create all notes with extracted content, ensure bidirectional links. + +**Example output for Sarah Chen:** +```markdown +# Sarah Chen + +## Info +**Role:** Engineering (led technical discussion in kickoff meeting) +**Organization:** [[Acme Corp]] +**Email:** sarah@acme.com +**Aliases:** Sarah, sarah@acme.com +**First met:** 2025-01-15 +**Last seen:** 2025-01-15 + +## Summary +Key contact at [[Acme Corp]] for the [[Acme Integration]] pilot. +Leading the technical evaluation. Reports to [[Jennifer]] (CTO). + +## Connected to +- [[Acme Corp]] — works at +- [[David Kim]] — colleague +- [[Jennifer]] — reports to (CTO) +- [[Acme Integration]] — key contact + +## Activity +- **2025-01-15** (meeting): Kickoff meeting for [[Acme Integration]]. Excited about pilot. [[David Kim]] needs API access first. [[Jennifer]] (CTO) joining next call. + +## Key facts +- Leading technical evaluation for pilot +- Needs API access to proceed +- CTO Jennifer involved in next steps + +## Open items +- [ ] Provide API access to [[David Kim]] +- [ ] Schedule follow-up call with [[Jennifer]] +``` + +**Example output for Acme Integration:** +```markdown +# Acme Integration + +## Info +**Type:** deal +**Status:** planning +**Started:** 2025-01-15 +**Last activity:** 2025-01-15 + +## Summary +Pilot integration project with [[Acme Corp]]. +Technical evaluation phase, working toward Q2 launch. + +## People +- [[Sarah Chen]] — key contact +- [[David Kim]] — technical lead +- [[Jennifer]] — CTO sponsor + +## Organizations +- [[Acme Corp]] — prospect + +## Timeline +**2025-01-15** (meeting) +Kickoff meeting. Team excited about pilot. API access needed first. CTO [[Jennifer]] joining next call. + +## Open items +- [ ] Provide API access to [[David Kim]] +- [ ] Schedule follow-up call with [[Jennifer]] +``` + +--- + +## Example 2: Email with State Changes + +**source_file:** `2025-01-20-email.md` ``` From: sarah@acme.com -To: me@company.com -Date: 2025-01-15 -Subject: Re: Pilot Timeline +To: arj@rowboat.com +Date: 2025-01-20 +Subject: Great news! -Hi, +Hi Arj, -Thanks for the call yesterday. I've discussed with David and we're -aligned on the Q2 timeline for the pilot. +Great news — leadership approved the pilot! Legal is drafting the +contract now. We should be ready to kick off by end of month. -One blocker: our security team needs SOC 2 certification before -we can proceed. Can you share your timeline on that? +Here's the API documentation you requested. -Jennifer (our CTO) wants to join the next call to discuss the -technical architecture. +Also, I've been promoted to VP of Engineering as of this month! Best, Sarah Chen VP Engineering, Acme Corp ``` -### Step 0: Filter Check +### Step 0: Determine Source Type + +`source_type = "email"` → Can only update existing notes + +### Step 1: Filter + +Check for existing relationship: ```bash -executeCommand("grep -r -i -l 'sarah chen' 'notes/'") +executeCommand("grep -r -i -l 'sarah@acme.com' 'notes/'") # Output: notes/People/Sarah Chen.md ``` -Known contact. Process. +Existing note found. Continue. -### Step 1: Parse and Extract Variants - -**Metadata:** -- Date: 2025-01-15 -- Type: email -- Title: Re: Pilot Timeline -- From: sarah@acme.com - -**Variants found:** -- People: "Sarah Chen", "Sarah", "sarah@acme.com", "David", "Jennifer", "our CTO" -- Organizations: "Acme Corp", "Acme", "@acme.com" -- Projects: "the pilot", "Q2 timeline" -- Topics: "SOC 2 certification", "security team" - -### Step 2: Search for Existing Notes -```bash -# People searches -executeCommand("grep -r -i -l 'Sarah Chen' 'notes/'") -# Output: notes/People/Sarah Chen.md, notes/Organizations/Acme Corp.md - -executeCommand("grep -r -i -l 'David' 'notes/People/'") -# Output: notes/People/David Kim.md, notes/People/David Chen.md - -executeCommand("grep -r -i -l 'Jennifer' 'notes/'") -# Output: (none) - -executeCommand("grep -r -i 'CTO' 'notes/People/'") -# Output: (none) - -# Organization searches -executeCommand("ls 'notes/Organizations/'") -# Output: Acme Corp.md, Friendly VC.md - -# Project searches -executeCommand("grep -r -i 'pilot\|integration' 'notes/Projects/'") -# Output: notes/Projects/Acme Integration.md - -# Topic searches -executeCommand("grep -r -i 'SOC 2\|security' 'notes/Topics/'") -# Output: (none) -``` - -**Read candidate notes:** -```bash -executeCommand("cat 'notes/People/Sarah Chen.md'") -# Shows: Organization: Acme Corp, Email: sarah@acme.com - -executeCommand("cat 'notes/People/David Kim.md'") -# Shows: Organization: Acme Corp - -executeCommand("cat 'notes/People/David Chen.md'") -# Shows: Organization: Other Corp - -executeCommand("cat 'notes/Organizations/Acme Corp.md'") -# Shows: Domain: acme.com - -executeCommand("cat 'notes/Projects/Acme Integration.md'") -# Shows: Type: deal, Organizations: Acme Corp -``` - -### Step 3: Resolve to Canonical Names - -**Disambiguation:** -- "David" — Two candidates. Check org context: - - David Kim → Acme Corp ✓ - - David Chen → Other Corp ✗ - - Source is from Acme → "David" = "David Kim" +### Steps 2-5: Parse, Search, Resolve, Skip **Resolution Map:** ``` RESOLVED: -- "Sarah Chen", "Sarah", "sarah@acme.com" → [[Sarah Chen]] -- "David" → [[David Kim]] -- "Acme Corp", "Acme", "@acme.com" → [[Acme Corp]] -- "the pilot", "Q2 timeline" → [[Acme Integration]] - -NEW ENTITIES: -- "Jennifer" (CTO at Acme Corp) → Create [[Jennifer]] -- "SOC 2 certification" → Create [[Security Compliance]] +- "Sarah", "sarah@acme.com" → [[Sarah Chen]] +- "Acme" → [[Acme Corp]] ``` -### Step 4: Identify New Entities +### Step 6: Extract Content -**Jennifer (CTO at Acme Corp):** -- Decision maker at prospect company -- "Would I prep?" → Yes -- Action: Create note +- Decision: Leadership approved pilot +- Commitment: Contract being drafted, kickoff by end of month +- Key fact: Legal involved, targeting end of month kickoff -**Security Compliance:** -- Recurring blocker topic -- Relevant to deal progression -- Action: Create topic note +### Step 7: Detect State Changes -### Step 5: Extract Content +**7a: Project Status:** +- "leadership approved the pilot" → Status: planning → active ✓ -**Decisions:** -- Q2 timeline agreed for pilot +**7b: Open Item Resolution:** +- "Here's the API documentation you requested" +- Existing open item: `- [ ] Send API documentation — by Friday` +- Match found → Mark complete ✓ -**Commitments:** -- [ ] Send SOC 2 certification timeline — you -- [ ] Schedule call with [[Jennifer]] — you +**7c: Role Change:** +- Signature: "VP Engineering" +- Existing: "Engineering" (inferred) +- Change detected → Update role ✓ -**Key facts:** -- [[Jennifer]] is CTO at [[Acme Corp]] -- [[Acme Corp]] security team needs SOC 2 before proceeding -- [[David Kim]] aligned with [[Sarah Chen]] on timeline +**7d: Relationship Change:** +- "Legal is drafting the contract" → Still prospect (not signed yet) +- No change -**Activity (using canonical names):** -- Sarah Chen: "Confirmed Q2 timeline with [[David Kim]]. [[Security Compliance|SOC 2]] blocker raised. [[Jennifer]] (CTO) joining next call." -- David Kim: "Aligned on Q2 timeline per [[Sarah Chen]]." -- Acme Corp: "Q2 timeline confirmed. Security team requires [[Security Compliance|SOC 2]]." -- Acme Integration: "Q2 timeline agreed. Blocked on [[Security Compliance]]. [[Jennifer]] joining technical review." - -### Step 6: Check Duplicates -```bash -executeCommand("grep '2025-01-15' 'notes/People/Sarah Chen.md'") -# Output: (none) +**State Change List:** +``` +STATE CHANGES: +- [[Acme Integration]]: Status planning → active +- [[Sarah Chen]]: Role "Engineering" → "VP Engineering" +- [[Sarah Chen]]: Open item "Provide API access" → completed (they sent docs) ``` -No duplicates. Proceed. +### Steps 8-10: Check, Write, Link -### Step 7: Write Updates - -**Update notes/People/Sarah Chen.md:** -- Add activity entry -- Update Last seen -- Add [[Jennifer]] to Connected to -- Add open items -- Add "Sarah" to Aliases if not present - -**Update notes/People/David Kim.md:** -- Add activity entry -- Update Last seen - -**Create notes/People/Jennifer.md:** +**Update Sarah Chen.md:** ```markdown -# Jennifer +# Sarah Chen ## Info -**Role:** CTO +**Role:** VP Engineering **Organization:** [[Acme Corp]] -**Email:** Unknown -**Aliases:** Jennifer -**First seen:** 2025-01-15 -**Last seen:** 2025-01-15 +**Email:** sarah@acme.com +**Aliases:** Sarah, sarah@acme.com +**First met:** 2025-01-15 +**Last seen:** 2025-01-20 ## Summary -CTO at [[Acme Corp]]. Interested in technical architecture discussions for [[Acme Integration]]. +VP Engineering at [[Acme Corp]] leading the [[Acme Integration]] pilot. +Key technical decision-maker. Recently promoted. ## Connected to -- [[Acme Corp]] — works at (CTO) -- [[Sarah Chen]] — colleague -- [[Acme Integration]] — stakeholder +- [[Acme Corp]] — works at +- [[David Kim]] — colleague +- [[Jennifer]] — reports to (CTO) +- [[Acme Integration]] — key contact ## Activity -- **2025-01-15** (email): [[Sarah Chen]] mentioned she wants to join next call for technical architecture discussion. +- **2025-01-20** (email): Leadership approved pilot. [Status → active] Legal drafting contract. Kickoff by end of month. Sent API documentation. [Role → VP Engineering] +- **2025-01-15** (meeting): Kickoff meeting for [[Acme Integration]]. Excited about pilot. [[David Kim]] needs API access first. [[Jennifer]] (CTO) joining next call. ## Key facts -- CTO at [[Acme Corp]] -- Interested in technical architecture details +- Leading technical evaluation for pilot +- Promoted to VP Engineering (Jan 2025) +- Legal drafting contract ## Open items -- [ ] Schedule technical call +- [x] Provide API access to [[David Kim]] — completed 2025-01-20 +- [ ] Schedule follow-up call with [[Jennifer]] ``` -**Update notes/Organizations/Acme Corp.md:** -- Add activity entry -- Add [[Jennifer]] to People section -- Update Last seen - -**Update notes/Projects/Acme Integration.md:** -- Add timeline entry -- Add decision -- Add [[Jennifer]] to People -- Add [[Security Compliance]] to Related -- Add open items - -**Create notes/Topics/Security Compliance.md:** +**Update Acme Integration.md:** ```markdown -# Security Compliance +# Acme Integration -## About -Security certifications and compliance requirements in customer evaluations. +## Info +**Type:** deal +**Status:** active +**Started:** 2025-01-15 +**Last activity:** 2025-01-20 -**Keywords:** SOC 2, security audit, compliance, certification -**Aliases:** SOC 2, SOC2, security certification -**First mentioned:** 2025-01-15 -**Last mentioned:** 2025-01-15 +## Summary +Pilot integration project with [[Acme Corp]]. +Leadership approved, contract in progress. Targeting end of month kickoff. -## Related -- [[Acme Corp]] — requires SOC 2 -- [[Acme Integration]] — blocked by this -- [[Sarah Chen]] — raised this requirement +## Timeline +**2025-01-20** (email) +Leadership approved pilot. [Status → active] Legal drafting contract. Targeting end of month kickoff. -## Log -**2025-01-15** (email: Re: Pilot Timeline) -[[Sarah Chen]] raised SOC 2 as blocker for [[Acme Integration]]. [[Acme Corp]] security team requires certification before proceeding. - -## Decisions -(none yet) - -## Open items -- [ ] Share SOC 2 certification timeline with [[Acme Corp]] - -## Key facts -- SOC 2 required by [[Acme Corp]] security team -- Currently blocking [[Acme Integration]] +**2025-01-15** (meeting) +Kickoff meeting. Team excited about pilot. API access needed first. CTO [[Jennifer]] joining next call. ``` -### Step 8: Verify Bidirectional Links -```bash -# Jennifer links to Acme Corp? -executeCommand("grep 'Acme Corp' 'notes/People/Jennifer.md'") -# ✓ Yes - -# Acme Corp lists Jennifer? -executeCommand("grep 'Jennifer' 'notes/Organizations/Acme Corp.md'") -# ✓ Yes (just added) - -# Security Compliance links to Acme Integration? -executeCommand("grep 'Acme Integration' 'notes/Topics/Security Compliance.md'") -# ✓ Yes - -# Acme Integration links to Security Compliance? -executeCommand("grep 'Security Compliance' 'notes/Projects/Acme Integration.md'") -# ✓ Yes (just added) -``` - -All bidirectional links verified. - --- -## Example 2: Transactional Vendor — Organization Note Only +## Example 3: Email — No Existing Relationship, Skip -**source_file:** `2025-01-15-email.md` +**source_file:** `2025-01-16-email.md` ``` -From: james.wong@hsbc.com -To: me@company.com -Cc: sarah.lee@hsbc.com -Date: 2025-01-15 -Subject: Re: Business Account Setup +From: sales@randomvendor.com +To: arj@rowboat.com +Date: 2025-01-16 +Subject: Quick question about your data needs Hi, -Your account is now active. - -Account Number: XXXX-1234 -Daily wire limit: $50,000 - -Sarah from support will help with limit increases. +I noticed your company is growing fast. Would love to show you +how we can help with your data infrastructure... Best, -James Wong -Relationship Manager, HSBC +John Smith ``` -### Resolution +### Step 0: Determine Source Type -**Variants:** James Wong, Sarah Lee, james.wong@hsbc.com, @hsbc.com, HSBC +`source_type = "email"` -**Search results:** +### Step 1: Filter + +Check for existing relationship: ```bash -executeCommand("grep -r -i -l 'hsbc' 'notes/'") +executeCommand("grep -r -i -l 'randomvendor' 'notes/'") +# Output: (none) + +executeCommand("grep -r -i -l 'John Smith' 'notes/'") # Output: (none) ``` -New organization. +No existing note. This is an email. Cannot create notes. -**"Would I prep?" test:** +**Output:** +``` +SKIP +Reason: No existing relationship (email from unknown contact) +``` + +--- + +## Example 4: Email — Warm Intro (Exception) + +**source_file:** `2025-01-16-email.md` +``` +From: david@friendly.vc +To: arj@rowboat.com +Cc: jennifer@newco.com +Date: 2025-01-16 +Subject: Intro: Jennifer Lee <> Arj + +Hey Arj, + +Want to introduce you to Jennifer Lee, CEO of NewCo. She's building +something interesting in your space and would love to chat. + +Jennifer — Arj is the founder of Rowboat, doing great work on AI agents. + +I'll let you two take it from here! + +David +``` + +### Step 0: Determine Source Type + +`source_type = "email"` + +### Step 1: Filter + +Check for sender: +```bash +executeCommand("grep -r -i -l 'david@friendly.vc' 'notes/'") +# Output: notes/People/David Park.md +``` + +Sender exists in memory. Check if this is a warm intro: +- Subject contains "Intro:" ✓ +- Body contains "introduce you to" ✓ +- New person (Jennifer Lee) is CC'd ✓ + +**This is a warm intro. Exception applies.** + +### Steps 2-4: Parse, Search, Resolve + +**Resolution Map:** +``` +RESOLVED: +- "David" → [[David Park]] (sender, exists) + +NEW ENTITIES (warm intro exception — create): +- "Jennifer Lee" → Create [[Jennifer Lee]] +- "NewCo" → Create [[NewCo]] +``` + +### Step 5: Create Notes (Exception) + +Even though this is an email, create notes for the introduced person. + +### Step 7: Detect State Changes + +No existing notes for Jennifer Lee / NewCo → No state changes. + +### Output + +Creates 2 new notes (Jennifer Lee, NewCo). Updates David Park.md with activity. + +--- + +## Example 5: Meeting — Transactional, Minimal Notes + +**source_file:** `2025-01-15-meeting.md` +``` +Meeting: HSBC Account Setup +Date: 2025-01-15 +Attendees: James Wong (james@hsbc.com), Sarah Lee (sarah.lee@hsbc.com), Arj + +Transcript: +James: Let's go through the account setup process. +Sarah: I'll handle the wire transfer limits after. +... +``` + +### Step 0: Determine Source Type + +`source_type = "meeting"` → Can create notes + +### Step 5: Identify New Entities + +Apply "Would I prep?" test: - James Wong (bank RM) → No - Sarah Lee (support) → No - HSBC (organization) → Yes, worth one org note **Action:** Create org note only, list people in Contacts section. -**Output:** +### Output ```markdown # HSBC @@ -1131,88 +1625,78 @@ New organization. **Relationship:** vendor (banking) **Domain:** hsbc.com **Aliases:** HSBC Bank -**First seen:** 2025-01-15 +**First met:** 2025-01-15 **Last seen:** 2025-01-15 ## Summary Business banking provider. Account setup completed January 2025. ## People -(none) ## Contacts - James Wong — Relationship Manager, account setup -- Sarah Lee — Support, handling limit increases - -## Projects -(none) +- Sarah Lee — Support, wire transfer limits ## Activity -- **2025-01-15** (email): Account activated. Wire limit $50K daily. +- **2025-01-15** (meeting): Account setup walkthrough. Wire transfer limits discussed. ## Key facts - Account Number: XXXX-1234 - Daily wire limit: $50,000 ## Open items -- [ ] Contact Sarah Lee for wire limit increase if needed ``` --- -## Example 3: Ambiguous Name Resolution +# Summary: The Core Rules -**source_file:** `2025-01-15-email.md` -``` -From: mike@acme.com -To: me@company.com -Subject: Quick question +| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? | +|-------------|---------------|----------------|------------------------| +| Meeting | Yes | Yes | Yes | +| Email (known contact) | No | Yes | Yes | +| Email (unknown contact) | No | No (SKIP) | No | +| Email (warm intro) | Yes (exception) | Yes | Yes | -Can you send me the latest deck? +--- -Mike +# State Change Reference + +## What Changes Automatically + +| Field | Trigger | Example | +|-------|---------|---------| +| Project Status | "approved", "on hold", "cancelled", "completed" | planning → active | +| Open Items | "here's the X you requested", "sent the X" | [ ] → [x] | +| Person Role | New title in signature, "promoted to" | Engineer → VP | +| Org Relationship | "signed contract", "now a customer" | prospect → customer | +| Person Organization | "I've joined X", new email domain | Acme → NewCo | + +## How to Log State Changes + +In activity entries, use `[Field → value]` notation: +```markdown +- **2025-01-20** (email): Leadership approved. [Status → active] Contract in progress. ``` -### Resolution +## When NOT to Change State -**Search:** -```bash -executeCommand("grep -r -i -l 'Mike' 'notes/People/'") -# Output: notes/People/Mike Chen.md, notes/People/Mike Johnson.md - -executeCommand("grep -i 'Acme' 'notes/People/Mike Chen.md'") -# Output: **Organization:** [[Acme Corp]] - -executeCommand("grep -i 'Acme' 'notes/People/Mike Johnson.md'") -# Output: **Organization:** [[Acme Corp]] -``` - -Two Mikes at Acme! Check email: -```bash -executeCommand("grep -i 'Email' 'notes/People/Mike Chen.md'") -# Output: **Email:** mike.chen@acme.com - -executeCommand("grep -i 'Email' 'notes/People/Mike Johnson.md'") -# Output: **Email:** mike@acme.com -``` - -**Resolution:** "mike@acme.com" exact match → Mike Johnson - -**Resolution Map:** -``` -- "Mike", "mike@acme.com" → [[Mike Johnson]] -``` +- Signal is ambiguous ("might move forward") +- Contradicts recent information (check activity log) +- Would be a regression (active → planning) +- Based on speculation, not explicit statement --- # Error Handling -1. **Missing data:** Use "Unknown", never leave blank -2. **Ambiguous names:** Create new note with "(possibly same as [[X]])" in key facts +1. **Missing data:** Leave blank rather than writing "Unknown" +2. **Ambiguous names:** For meetings, create note with "(possibly same as [[X]])". For emails, skip. 3. **Conflicting info:** Note both versions, mark "needs clarification" -4. **grep returns nothing:** Apply qualifying rules, create if appropriate -5. **Note file malformed:** Log warning, attempt partial update, continue -6. **Shell command fails:** Log error, continue with what you have +4. **grep returns nothing:** For meetings, apply qualifying rules and create if appropriate. For emails, skip. +5. **State change unclear:** Log in activity but don't change the field +6. **Note file malformed:** Log warning, attempt partial update, continue +7. **Shell command fails:** Log error, continue with what you have --- @@ -1220,6 +1704,10 @@ executeCommand("grep -i 'Email' 'notes/People/Mike Johnson.md'") Before completing, verify: +**Source Type:** +- [ ] Correctly identified as meeting or email +- [ ] Applied correct rules (meetings create, emails only update) + **Resolution:** - [ ] Extracted all name variants from source - [ ] Searched notes including Aliases fields @@ -1228,19 +1716,30 @@ Before completing, verify: - [ ] Updated Aliases fields with new variants discovered **Filtering:** +- [ ] Excluded self (user.name, user.email, @user.domain) - [ ] Applied "Would I prep?" test to each person - [ ] Transactional contacts in Org Contacts, not People notes - [ ] Source correctly classified (process vs skip) +- [ ] Emails from unknown contacts skipped (unless warm intro) -**Content:** -- [ ] All entity mentions are `[[linked]]` with canonical names -- [ ] Activity entries are reverse chronological -- [ ] Summaries are 2-3 sentences max -- [ ] Key facts are specific and not duplicated -- [ ] Open items are actionable -- [ ] Decisions include rationale +**Content Quality:** +- [ ] Summaries describe relationship, not communication method +- [ ] Roles inferred where possible (with qualifier) +- [ ] Key facts are substantive (no "name only provided" filler) +- [ ] Open items are commitments/next steps (no "find their email" tasks) +- [ ] Empty sections left empty rather than filled with placeholders + +**State Changes:** +- [ ] Detected project status changes +- [ ] Marked completed open items with [x] +- [ ] Updated roles if changed +- [ ] Updated relationships if changed +- [ ] Logged all state changes in activity with [Field → value] notation +- [ ] Only applied clear, unambiguous state changes **Structure:** +- [ ] All entity mentions are `[[linked]]` with canonical names +- [ ] Activity entries are reverse chronological - [ ] No duplicate activity entries - [ ] Dates are YYYY-MM-DD - [ ] Bidirectional links are consistent diff --git a/apps/x/packages/core/src/knowledge/sync_fireflies.ts b/apps/x/packages/core/src/knowledge/sync_fireflies.ts index e3b08413..bb74eaf2 100644 --- a/apps/x/packages/core/src/knowledge/sync_fireflies.ts +++ b/apps/x/packages/core/src/knowledge/sync_fireflies.ts @@ -2,13 +2,15 @@ import fs from 'fs'; import path from 'path'; import { WorkDir } from '../config/config.js'; import { FirefliesClientFactory } from './fireflies-client-factory.js'; -import { buildGraph } from './build_graph.js'; // Configuration const SYNC_DIR = path.join(WorkDir, 'fireflies_transcripts'); -const SYNC_INTERVAL_MS = 60 * 1000; // Check every minute +const SYNC_INTERVAL_MS = 30 * 60 * 1000; // Check every 30 minutes (reduced from 1 minute) const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); const LOOKBACK_DAYS = 30; // Last 1 month +const API_DELAY_MS = 2000; // 2 second delay between API calls +const RATE_LIMIT_RETRY_DELAY_MS = 60 * 1000; // Wait 1 minute on rate limit +const MAX_RETRIES = 3; // Maximum retries for rate-limited requests // --- Types for Fireflies API responses --- @@ -77,6 +79,56 @@ interface McpToolResult { // --- Helper Functions --- +/** + * Sleep for a specified number of milliseconds + */ +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Execute an API call with rate limit handling and exponential backoff + */ +async function callWithRateLimit( + operation: () => Promise, + operationName: string +): Promise { + let retries = 0; + let delay = RATE_LIMIT_RETRY_DELAY_MS; + + while (retries < MAX_RETRIES) { + try { + const result = await operation(); + return result; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // Check if it's a rate limit error (429 Too Many Requests) + if (errorMessage.includes('429') || + errorMessage.includes('Too Many Requests') || + errorMessage.includes('too many requests') || + errorMessage.includes('rate limit')) { + + retries++; + console.log(`[Fireflies] Rate limit hit for ${operationName}. Retry ${retries}/${MAX_RETRIES} in ${delay/1000}s...`); + + if (retries >= MAX_RETRIES) { + console.error(`[Fireflies] Max retries reached for ${operationName}. Skipping.`); + return null; + } + + await sleep(delay); + delay *= 2; // Exponential backoff + } else { + // Not a rate limit error, throw it + throw error; + } + } + } + + return null; +} + function cleanFilename(name: string): string { return name.replace(/[\\/*?:"<>|]/g, "_").substring(0, 100).trim(); } @@ -95,7 +147,11 @@ function formatTimestamp(seconds?: number): string { return `[${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}]`; } -function loadState(): { lastSyncDate?: string; syncedIds?: string[] } { +function loadState(): { + lastSyncDate?: string; + syncedIds?: string[]; + lastCheckTime?: string; +} { if (fs.existsSync(STATE_FILE)) { try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); @@ -106,10 +162,11 @@ function loadState(): { lastSyncDate?: string; syncedIds?: string[] } { return {}; } -function saveState(lastSyncDate: string, syncedIds: string[]) { +function saveState(lastSyncDate: string, syncedIds: string[], lastCheckTime?: string) { fs.writeFileSync(STATE_FILE, JSON.stringify({ lastSyncDate, syncedIds, + lastCheckTime: lastCheckTime || new Date().toISOString(), last_sync: new Date().toISOString() }, null, 2)); } @@ -296,46 +353,68 @@ function meetingToMarkdown(meeting: FirefliesMeetingData): string { async function syncMeetings() { console.log('[Fireflies] Starting sync...'); - + // Ensure sync directory exists if (!fs.existsSync(SYNC_DIR)) { fs.mkdirSync(SYNC_DIR, { recursive: true }); } - + const client = await FirefliesClientFactory.getClient(); if (!client) { console.log('[Fireflies] No valid client available'); return; } - + const state = loadState(); const syncedIds = new Set(state.syncedIds || []); - + + // Skip if we checked very recently (within 5 minutes) + if (state.lastCheckTime) { + const lastCheck = new Date(state.lastCheckTime); + const now = new Date(); + const minutesSinceLastCheck = (now.getTime() - lastCheck.getTime()) / (1000 * 60); + + if (minutesSinceLastCheck < 5) { + console.log(`[Fireflies] Skipping - last check was ${minutesSinceLastCheck.toFixed(1)} minutes ago`); + return; + } + } + // Calculate date range (last 30 days) const toDate = new Date(); const fromDate = new Date(); fromDate.setDate(fromDate.getDate() - LOOKBACK_DAYS); - + const fromDateStr = fromDate.toISOString().split('T')[0]; // YYYY-MM-DD const toDateStr = toDate.toISOString().split('T')[0]; - + console.log(`[Fireflies] Fetching meetings from ${fromDateStr} to ${toDateStr}...`); - + try { - // Step 1: Get list of transcripts - const transcriptsResult = await client.callTool({ - name: 'fireflies_get_transcripts', - arguments: { - fromDate: fromDateStr, - toDate: toDateStr, - limit: 50, - format: 'json', - }, - }) as McpToolResult; + // Step 1: Get list of transcripts with rate limiting + const transcriptsResult = await callWithRateLimit( + async () => client.callTool({ + name: 'fireflies_get_transcripts', + arguments: { + fromDate: fromDateStr, + toDate: toDateStr, + limit: 50, + format: 'json', + }, + }) as McpToolResult, + 'get_transcripts' + ); + // Handle rate-limited failure + if (!transcriptsResult) { + console.log('[Fireflies] Failed to fetch transcripts due to rate limit'); + saveState(toDateStr, Array.from(syncedIds), new Date().toISOString()); + return; + } + // Parse result - API returns array directly, not { transcripts: [...] } const parsedData = parseMcpResult(transcriptsResult); - + // Handle both array and object responses let meetings: FirefliesMeeting[]; if (Array.isArray(parsedData)) { @@ -345,10 +424,10 @@ async function syncMeetings() { } else { meetings = []; } - + if (meetings.length === 0) { console.log('[Fireflies] No transcripts found in date range'); - saveState(toDateStr, Array.from(syncedIds)); + saveState(toDateStr, Array.from(syncedIds), new Date().toISOString()); return; } @@ -356,44 +435,66 @@ async function syncMeetings() { // Step 2: Fetch and save each transcript let newCount = 0; + let processedInBatch = 0; + const MAX_BATCH_SIZE = 5; // Process max 5 new transcripts per sync to avoid rate limits + for (const meeting of meetings) { const meetingId = meeting.id; - + // Skip if already synced if (syncedIds.has(meetingId)) { console.log(`[Fireflies] Skipping already synced: ${meeting.title || meetingId}`); continue; } - + + // Limit batch size to avoid too many API calls + if (processedInBatch >= MAX_BATCH_SIZE) { + console.log(`[Fireflies] Reached batch limit (${MAX_BATCH_SIZE}), will continue in next sync`); + break; + } + + // Add delay between API calls to respect rate limits + if (processedInBatch > 0) { + console.log(`[Fireflies] Waiting ${API_DELAY_MS/1000}s before next API call...`); + await sleep(API_DELAY_MS); + } + try { console.log(`[Fireflies] Fetching full transcript: ${meeting.title || meetingId}`); - - // Try to get transcript sentences using fireflies_get_transcript + + // Try to get transcript sentences using fireflies_get_transcript with rate limiting let sentences: FirefliesTranscriptSentence[] = []; try { - const transcriptResult = await client.callTool({ - name: 'fireflies_get_transcript', - arguments: { - transcriptId: meetingId, - }, - }) as McpToolResult; + const transcriptResult = await callWithRateLimit( + async () => client.callTool({ + name: 'fireflies_get_transcript', + arguments: { + transcriptId: meetingId, + }, + }) as McpToolResult, + `get_transcript_${meetingId}` + ); - // Try JSON first - const transcriptData = parseMcpResult<{ sentences?: FirefliesTranscriptSentence[] } | FirefliesTranscriptSentence[]>(transcriptResult); - - if (transcriptData) { - if (Array.isArray(transcriptData)) { - sentences = transcriptData; - } else if (transcriptData.sentences) { - sentences = transcriptData.sentences; + if (transcriptResult) { + // Try JSON first + const transcriptData = parseMcpResult<{ sentences?: FirefliesTranscriptSentence[] } | FirefliesTranscriptSentence[]>(transcriptResult); + + if (transcriptData) { + if (Array.isArray(transcriptData)) { + sentences = transcriptData; + } else if (transcriptData.sentences) { + sentences = transcriptData.sentences; + } + } else { + // Try parsing toon format + const rawText = getRawText(transcriptResult); + if (rawText) { + sentences = parseToonTranscript(rawText); + console.log(`[Fireflies] Parsed ${sentences.length} sentences from toon format`); + } } } else { - // Try parsing toon format - const rawText = getRawText(transcriptResult); - if (rawText) { - sentences = parseToonTranscript(rawText); - console.log(`[Fireflies] Parsed ${sentences.length} sentences from toon format`); - } + console.log(`[Fireflies] Skipping transcript due to rate limit: ${meetingId}`); } } catch (err) { console.log(`[Fireflies] Could not fetch transcript sentences: ${err}`); @@ -420,29 +521,20 @@ async function syncMeetings() { fs.writeFileSync(filePath, markdown); console.log(`[Fireflies] Saved: ${filename}`); - + syncedIds.add(meetingId); newCount++; + processedInBatch++; } catch (error) { console.error(`[Fireflies] Error fetching meeting ${meetingId}:`, error); // Continue with next meeting } } - - console.log(`[Fireflies] Synced ${newCount} new transcripts`); - - // Save state - saveState(toDateStr, Array.from(syncedIds)); - - // Build knowledge graph after successful sync - if (newCount > 0) { - console.log('\n[Fireflies] Starting knowledge graph build...'); - try { - await buildGraph(SYNC_DIR); - } catch (error) { - console.error('[Fireflies] Error building knowledge graph:', error); - } - } + + console.log(`[Fireflies] Synced ${newCount} new transcripts in this batch`); + + // Save state with updated timestamp + saveState(toDateStr, Array.from(syncedIds), new Date().toISOString()); } catch (error) { console.error('[Fireflies] Error during sync:', error); diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts index e980996a..945b5999 100644 --- a/apps/x/packages/core/src/knowledge/sync_gmail.ts +++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts @@ -4,7 +4,6 @@ import { google, gmail_v1 as gmail } from 'googleapis'; import { NodeHtmlMarkdown } from 'node-html-markdown' import { OAuth2Client } from 'google-auth-library'; import { WorkDir } from '../config/config.js'; -import { buildGraph } from './build_graph.js'; import { GoogleClientFactory } from './google-client-factory.js'; // Configuration @@ -281,14 +280,6 @@ async function performSync() { } console.log("Sync completed."); - - // Build knowledge graph after successful sync - console.log("\nStarting knowledge graph build..."); - try { - await buildGraph(SYNC_DIR); - } catch (error) { - console.error("Error building knowledge graph:", error); - } } catch (error) { console.error("Error during sync:", error); }