This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Letta API URL discovery for carry/vault notes without URLs

When a carry or vault note has no https:// URL, the analysis now calls
the Letta API (url-discoverer agent with tavily_search tool) to find
the canonical URL. This ensures all connection records have valid
https:// targets.

- Created url-discoverer agent (agent-36607e8a) on Letta Cloud
- Container calls Letta API for notes lacking URLs during analysis
- Secrets: LETTA_API_KEY, LETTA_AGENT_ID in Cloudflare
- Fixed async/await for findFilesystemTouchpoints (was returning Promise)
- Health endpoint shows lettaKeySet and lettaAgentId

Test: 14 connections found for single-vertex island, all with https://
targets discovered via Letta web search.

๐Ÿ‘พ Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

author
nandi
co-author
Letta Code
date (May 14, 2026, 8:14 AM UTC) commit e305a384 parent fbc7242e
+81 -10
+72 -9
container/analysis.ts
··· 2 2 // Cross-references an island (cluster of linked URLs) against: 3 3 // 1. Vault + carry markdown from local filesystem (synced via ob/git) 4 4 // 2. Structured carry records from D1 (entities, citations, reasonings) 5 + // For carry/vault notes without URLs, uses the Letta API to discover 6 + // canonical URLs via web search. 5 7 // Returns structured analysis + new connection records to be created on PDS. 6 8 7 9 import { readdirSync, readFileSync, statSync } from "fs"; ··· 9 11 10 12 const VAULT_PATH = process.env.VAULT_PATH || "/data/vault"; 11 13 const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 14 + const LETTA_API_KEY = process.env.LETTA_API_KEY || ""; 15 + const LETTA_BASE_URL = process.env.LETTA_BASE_URL || "https://api.letta.com"; 16 + const LETTA_AGENT_ID = process.env.LETTA_AGENT_ID || ""; 12 17 13 18 // โ”€โ”€โ”€ Types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 14 19 ··· 113 118 const newConnections = findCarryTouchpoints(island, carryData || { entities: [], citations: [], reasonings: [] }); 114 119 115 120 // 5. Find vault/carry touchpoints โ€” semantic matches from filesystem notes 116 - const fsConnections = findFilesystemTouchpoints(island, vaultNotes, carryNotes); 121 + // (async โ€” may call Letta API for URL discovery) 122 + const fsConnections = await findFilesystemTouchpoints(island, vaultNotes, carryNotes); 117 123 newConnections.push(...fsConnections); 118 124 119 125 // 6. Flag hot edges โ€” existing edges that are semantically significant ··· 289 295 return connections; 290 296 } 291 297 298 + // โ”€โ”€โ”€ Letta URL discovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 299 + // For carry/vault notes that lack an https:// URL, ask the Letta agent 300 + // to find the canonical URL via web search. 301 + 302 + async function discoverUrl(semanticDescription: string): Promise<string | null> { 303 + if (!LETTA_API_KEY || !LETTA_AGENT_ID) return null; 304 + 305 + try { 306 + const resp = await fetch(`${LETTA_BASE_URL}/v1/agents/${LETTA_AGENT_ID}/messages`, { 307 + method: "POST", 308 + headers: { 309 + "Authorization": `Bearer ${LETTA_API_KEY}`, 310 + "Content-Type": "application/json", 311 + }, 312 + body: JSON.stringify({ 313 + messages: [{ 314 + role: "user", 315 + content: `Find the canonical URL for: ${semanticDescription}`, 316 + }], 317 + }), 318 + }); 319 + 320 + if (!resp.ok) { 321 + console.error(`[letta] API error: ${resp.status}`); 322 + return null; 323 + } 324 + 325 + const data = await resp.json() as { messages: Array<{ message_type: string; content?: string }> }; 326 + // Find the assistant message โ€” it should contain just the URL 327 + for (const msg of data.messages || []) { 328 + if (msg.message_type === "assistant_message" && msg.content) { 329 + // Extract URL from the response 330 + const urlMatch = msg.content.match(/https?:\/\/[^\s)\]]+/); 331 + if (urlMatch) return urlMatch[0]; 332 + } 333 + } 334 + return null; 335 + } catch (e: any) { 336 + console.error("[letta] discoverUrl error:", e.message); 337 + return null; 338 + } 339 + } 340 + 292 341 // โ”€โ”€โ”€ Filesystem touchpoint matching (vault + carry notes) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 293 342 294 343 function extractUrl(content: string): string | null { ··· 297 346 return match ? match[0] : null; 298 347 } 299 348 300 - function findFilesystemTouchpoints( 349 + function extractTitle(content: string): string { 350 + return content.split("\n").find(l => l.startsWith("#"))?.replace(/^#+\s*/, "") || ""; 351 + } 352 + 353 + async function findFilesystemTouchpoints( 301 354 island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 302 355 vaultNotes: VaultNote[], 303 356 carryNotes: CarryNote[], 304 - ): NewConnection[] { 357 + ): Promise<NewConnection[]> { 305 358 const connections: NewConnection[] = []; 306 359 const islandText = buildIslandText(island).toLowerCase(); 307 360 ··· 311 364 const overlap = keywordOverlap(islandText, noteLower); 312 365 if (overlap.length < 3) continue; 313 366 314 - const noteUrl = extractUrl(note.content); 315 - if (!noteUrl) continue; // No URL = can't form a connection 367 + let noteUrl = extractUrl(note.content); 368 + const title = extractTitle(note.content) || note.path; 369 + 370 + // If no URL in the note, ask Letta to discover one 371 + if (!noteUrl && LETTA_API_KEY && LETTA_AGENT_ID) { 372 + noteUrl = await discoverUrl(title || note.path); 373 + } 374 + if (!noteUrl) continue; 316 375 317 376 const bestVertex = findBestVertexMatch(noteLower, island.vertices); 318 377 if (bestVertex) { 319 - const title = note.content.split("\n").find(l => l.startsWith("#"))?.replace(/^#+\s*/, "") || note.path; 320 378 connections.push({ 321 379 source: bestVertex.uri, 322 380 target: noteUrl, ··· 332 390 const overlap = keywordOverlap(islandText, noteLower); 333 391 if (overlap.length < 2) continue; 334 392 335 - const noteUrl = extractUrl(note.content); 336 - if (!noteUrl) continue; // No URL = can't form a connection 393 + let noteUrl = extractUrl(note.content); 394 + const title = extractTitle(note.content) || note.path; 395 + 396 + // If no URL in the note, ask Letta to discover one 397 + if (!noteUrl && LETTA_API_KEY && LETTA_AGENT_ID) { 398 + noteUrl = await discoverUrl(`Carry [${note.domain}]: ${title}`); 399 + } 400 + if (!noteUrl) continue; 337 401 338 402 const bestVertex = findBestVertexMatch(noteLower, island.vertices); 339 403 if (bestVertex) { 340 - const title = note.content.split("\n").find(l => l.startsWith("#"))?.replace(/^#+\s*/, "") || note.path; 341 404 connections.push({ 342 405 source: bestVertex.uri, 343 406 target: noteUrl,
+2
container/server.ts
··· 57 57 carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN, 58 58 obsidianTokenSet: !!process.env.OBSIDIAN_AUTH_TOKEN, 59 59 obsidianConfigDir: existsSync("/root/.config/obsidian-headless/auth_token"), 60 + lettaKeySet: !!process.env.LETTA_API_KEY, 61 + lettaAgentId: process.env.LETTA_AGENT_ID || "not set", 60 62 })); 61 63 return; 62 64 }
+5 -1
src/container.ts
··· 6 6 OBSIDIAN_AUTH_TOKEN: string; 7 7 OBSIDIAN_ENCRYPTION_KEY: string; 8 8 OBSIDIAN_ENCRYPTION_SALT: string; 9 + LETTA_API_KEY: string; 10 + LETTA_AGENT_ID: string; 9 11 } 10 12 11 13 export class StrataContainer extends Container<ContainerEnv> { ··· 26 28 OBSIDIAN_SYNC_HOST: "sync-53.obsidian.md", 27 29 ...(env.OBSIDIAN_ENCRYPTION_KEY ? { OBSIDIAN_ENCRYPTION_KEY: env.OBSIDIAN_ENCRYPTION_KEY } : {}), 28 30 ...(env.OBSIDIAN_ENCRYPTION_SALT ? { OBSIDIAN_ENCRYPTION_SALT: env.OBSIDIAN_ENCRYPTION_SALT } : {}), 31 + ...(env.LETTA_API_KEY ? { LETTA_API_KEY: env.LETTA_API_KEY } : {}), 32 + ...(env.LETTA_AGENT_ID ? { LETTA_AGENT_ID: env.LETTA_AGENT_ID } : {}), 29 33 }; 30 34 } 31 35 32 36 async onStart() { 33 37 const has = (k: string) => !!this.envVars?.[k]; 34 - console.log("[strata-container] started, carry:", has("CARRY_GITHUB_TOKEN") ? "set" : "missing", "obsidian:", has("OBSIDIAN_AUTH_TOKEN") ? "set" : "missing"); 38 + console.log("[strata-container] started, carry:", has("CARRY_GITHUB_TOKEN") ? "set" : "missing", "obsidian:", has("OBSIDIAN_AUTH_TOKEN") ? "set" : "missing", "letta:", has("LETTA_API_KEY") ? "set" : "missing"); 35 39 } 36 40 37 41 async onStop() {
+2
src/worker.ts
··· 19 19 OBSIDIAN_AUTH_TOKEN: string; 20 20 OBSIDIAN_ENCRYPTION_KEY: string; 21 21 OBSIDIAN_ENCRYPTION_SALT: string; 22 + LETTA_API_KEY: string; 23 + LETTA_AGENT_ID: string; 22 24 } 23 25 24 26 // โ”€โ”€โ”€ Contrail setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€