@@ -7,8 +7,6 @@ export type Memory = {
description : string ;
description : string ;
content : string ;
content : string ;
embedding : number [ ] ;
embedding : number [ ] ;
links : string [ ] ;
backlinks : string [ ] ;
}
}
type MemoryRef = {
type MemoryRef = {
@@ -19,6 +17,7 @@ type MemoryRef = {
type FactBucket = {
type FactBucket = {
subject : string ;
subject : string ;
facts : string [ ] ;
facts : string [ ] ;
isNew : boolean ;
}
}
export type MemoryNode = {
export type MemoryNode = {
@@ -33,26 +32,31 @@ export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[]
const nameSet = new Set ( mems . map ( m = > m . name ) ) ;
const nameSet = new Set ( mems . map ( m = > m . name ) ) ;
const ghosts = new Set < string > ( ) ;
const ghosts = new Set < string > ( ) ;
for ( const m of mems ) {
const nodes : MemoryNode [ ] = mems . map ( m = > {
for ( const link of m . links ) {
const { links , backlinks } = extractMetadata ( m . content ) ;
return {
name : m.name ,
missing : false ,
links ,
backlinks ,
} ;
} ) ;
for ( const node of nodes ) {
for ( const link of node . links ) {
if ( ! nameSet . has ( link ) ) ghosts . add ( link ) ;
if ( ! nameSet . has ( link ) ) ghosts . add ( link ) ;
}
}
}
}
return [
return [
. . . mems . map ( m = > ( {
. . . nodes ,
name : m.name ,
missing : false ,
links : m.links ,
backlinks : m.backlinks ,
} ) ) ,
. . . [ . . . ghosts ] . map ( name = > ( {
. . . [ . . . ghosts ] . map ( name = > ( {
name ,
name ,
missing : true ,
missing : true ,
links : [ ] ,
links : [ ] ,
backlinks : mem s
backlinks : node s
. filter ( m = > m . links . includes ( name ) )
. filter ( n = > n . links . includes ( name ) )
. map ( m = > m . name ) ,
. map ( n = > n . name ) ,
} ) )
} ) )
] ;
] ;
}
}
@@ -62,14 +66,21 @@ function extractLinks(content: string): string[] {
return [ . . . new Set ( [ . . . matches ] . map ( m = > m [ 1 ] . trim ( ) ) ) ] ;
return [ . . . new Set ( [ . . . matches ] . map ( m = > m [ 1 ] . trim ( ) ) ) ] ;
}
}
function rebuildBacklinks ( memories : Memory [ ] ) : void {
export function extractMetadata ( content : string ) : { links : string [ ] , backlinks : string [ ] } {
for ( const m of memories ) m . backlinks = [ ] ;
const match = content . match ( /^---\n([\s\S]*?)\n---/ ) ;
for ( const m of memories ) {
i f ( ! match ) return { links : [ ] , backlinks : [ ] } ;
for ( const link of m . links ) {
const target = memories . find ( t = > t . name === link ) ;
const fm = match [ 1 ] ;
if ( target ) target . backlinks . push ( m . name ) ;
const getList = ( key : string ) : string [ ] = > {
}
const m = fm . match ( new RegExp ( ` ^ ${ key } : \\ s* \\ [(.*) \\ ] $ ` , 'm' ) ) ;
}
if ( ! m || ! m [ 1 ] . trim ( ) ) return [ ] ;
return m [ 1 ] . split ( ',' ) . map ( s = > s . trim ( ) . replace ( /^"|"$/g , '' ) ) . filter ( Boolean ) ;
} ;
return {
links : getList ( 'links' ) ,
backlinks : getList ( 'backlinks' ) ,
} ;
}
}
function cosineDistance ( a : number [ ] , b : number [ ] ) : number {
function cosineDistance ( a : number [ ] , b : number [ ] ) : number {
@@ -83,9 +94,53 @@ function cosineDistance(a: number[], b: number[]): number {
return denom === 0 ? 1 : 1 - dot / denom ;
return denom === 0 ? 1 : 1 - dot / denom ;
}
}
function getWeekMonday ( date : Date = new Date ( ) ) : string {
const d = new Date ( Date . UTC ( date . getFullYear ( ) , date . getMonth ( ) , date . getDate ( ) ) ) ;
const day = d . getUTCDay ( ) ;
const diff = day === 0 ? - 6 : 1 - day ;
d . setUTCDate ( d . getUTCDate ( ) + diff ) ;
return d . toISOString ( ) . slice ( 0 , 10 ) ;
}
function getWeekSunday ( monday : string ) : string {
const d = new Date ( ` ${ monday } T00:00:00Z ` ) ;
d . setUTCDate ( d . getUTCDate ( ) + 6 ) ;
return d . toISOString ( ) . slice ( 0 , 10 ) ;
}
function tagsFromName ( name : string ) : string [ ] {
const prefix = name . split ( '/' ) [ 0 ] ;
return prefix ? [ prefix . toLowerCase ( ) ] : [ ] ;
}
export function serializeMemory ( mem : Memory , week ? : { monday : string , sunday : string } ) : string {
return mem . content ;
}
export function deserializeMemory ( raw : string , embedding : number [ ] = [ ] ) : Memory {
const match = raw . match ( /^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/ ) ;
if ( ! match ) {
return { name : '' , description : '' , content : raw.trim ( ) , embedding } ;
}
const [ , fm ] = match ;
const get = ( key : string ) : string = > {
const m = fm . match ( new RegExp ( ` ^ ${ key } : \\ s*(.+) $ ` , 'm' ) ) ;
return m ? m [ 1 ] . trim ( ) : '' ;
} ;
return {
name : get ( 'name' ) ,
description : get ( 'description' ) ,
content : raw.trim ( ) ,
embedding ,
} ;
}
export class MemoryCache {
export class MemoryCache {
private tree : KDTree < MemoryRef > ;
private tree : KDTree < MemoryRef > ;
public memories : Memory [ ] ;
public memories : Memory [ ] ;
private locks = new Map < string , Promise < void > > ( ) ;
constructor ( memories : Memory [ ] ) {
constructor ( memories : Memory [ ] ) {
this . memories = memories ;
this . memories = memories ;
@@ -125,16 +180,38 @@ export class MemoryCache {
this . rebuild ( ) ;
this . rebuild ( ) ;
}
}
remove ( name : string ) : void {
const idx = this . memories . findIndex ( m = > m . name === name ) ;
if ( idx !== - 1 ) {
this . memories . splice ( idx , 1 ) ;
this . rebuild ( ) ;
}
}
rebuild ( ) : void {
rebuild ( ) : void {
this . tree = this . buildTree ( ) ;
this . tree = this . buildTree ( ) ;
}
}
rebuildLinks ( ) : void {
lock < T > ( name : string , fn : ( ) = > Promise < T > ) : Promise < T > {
rebuildBacklinks ( th is. memories ) ;
const prev = this . locks . get ( name ) ? ? Prom ise . resolve ( ) ;
let resolveLock ! : ( ) = > void ;
const next = new Promise < void > ( r = > { resolveLock = r ; } ) ;
this . locks . set ( name , next ) ;
const result = prev . then ( fn ) . finally ( resolveLock ) ;
result . finally ( ( ) = > {
if ( this . locks . get ( name ) === next ) this . locks . delete ( name ) ;
} ) ;
return result ;
}
}
}
}
export class MemoryManager {
export class MemoryManager {
private pendingMemorizations = new Map < string , {
memories : Memory [ ] | MemoryCache ,
tempMemoryName : string ,
timestamp : number ,
} > ( ) ;
tools = {
tools = {
read : ( memories : Memory [ ] | MemoryCache ) : AiTool = > ( {
read : ( memories : Memory [ ] | MemoryCache ) : AiTool = > ( {
@@ -147,19 +224,78 @@ export class MemoryManager {
const mems = memories instanceof MemoryCache ? memories.memories : memories ;
const mems = memories instanceof MemoryCache ? memories.memories : memories ;
const mem = mems . find ( m = > m . name === args . name ) ;
const mem = mems . find ( m = > m . name === args . name ) ;
if ( ! mem ) return 'Document not found' ;
if ( ! mem ) return 'Document not found' ;
return this . formatMemory ( mem ) ;
return mem . content ;
}
} ,
} ) ,
forget : ( memories : Memory [ ] | MemoryCache ) : AiTool = > ( {
name : 'forget_memory' ,
description : 'Permanently delete a memory document and clean up all references to it' ,
args : {
name : { type : 'string' , description : 'Exact memory name to forget' , required : true } ,
reason : { type : 'string' , description : 'Why this memory is being deleted' , required : true } ,
} ,
fn : ( args : any ) = > {
const result = this . forget ( args . name , memories ) ;
return result ? ` Forgotten: ${ args . name } ` : ` Not found: ${ args . name } ` ;
} ,
} ) ,
} ) ,
} ;
} ;
constructor ( private llm : any ) { }
constructor ( private llm : any ) { }
private async createTempMemory ( conversation : string ) : Promise < Memory > {
const [ e ] = await this . llm . embedding ( conversation ) ;
const timestamp = Date . now ( ) ;
return {
name : ` _temp_ ${ timestamp } ` ,
description : 'Temporary memory - processing in background' ,
content : ` ---
name: _temp_ ${ timestamp }
description: Temporary memory - processing in background
tags: [_temporary]
links: []
backlinks: []
modified: ${ new Date ( ) . toISOString ( ) }
---
# Recent Conversation (Processing)
${ conversation } ` ,
embedding : e?.embedding || [ ] ,
} ;
}
forget ( name : string , memories : Memory [ ] | MemoryCache ) : boolean {
const mem = memories instanceof MemoryCache ? memories.memories : memories ;
const idx = mem . findIndex ( m = > m . name === name ) ;
if ( idx === - 1 ) return false ;
for ( const node of mem ) {
const { links , backlinks } = extractMetadata ( node . content ) ;
const newBacklinks = backlinks . filter ( b = > b !== name ) ;
const newLinks = links . filter ( l = > l !== name ) ;
if ( newBacklinks . length !== backlinks . length || newLinks . length !== links . length ) {
node . content = this . updateFrontmatter ( node . content , {
links : newLinks ,
backlinks : newBacklinks ,
} ) ;
}
}
mem . splice ( idx , 1 ) ;
if ( memories instanceof MemoryCache ) memories . rebuild ( ) ;
return true ;
}
private cosineSearch ( query : number [ ] , memories : Memory [ ] , limit : number ) : MemoryRef [ ] {
private cosineSearch ( query : number [ ] , memories : Memory [ ] , limit : number ) : MemoryRef [ ] {
const scored = memories
const scored = memories
. filter ( m = > m . embedding ? . length )
. filter ( m = > m . embedding ? . length )
. map ( m = > ( {
. map ( m = > ( {
ref : { name : m.name , description : m.description } ,
ref : { name : m.name , description : m.description } ,
distance : cosineDistance ( query , m . embedding )
distance : cosineDistance ( query , m . embedding ) ,
} ) )
} ) )
. sort ( ( a , b ) = > a . distance - b . distance )
. sort ( ( a , b ) = > a . distance - b . distance )
. slice ( 0 , limit ) ;
. slice ( 0 , limit ) ;
@@ -174,22 +310,9 @@ export class MemoryManager {
description : '' ,
description : '' ,
content : '' ,
content : '' ,
embedding : [ ] ,
embedding : [ ] ,
links : [ ] ,
backlinks : [ ] ,
} ;
} ;
}
}
private formatMemory ( mem : Memory ) : string {
return [
` # ${ mem . name } ` ,
mem . description ? ` > ${ mem . description } ` : '' ,
mem . links . length ? ` **Links:** ${ mem . links . map ( l = > ` [[ ${ l } ]] ` ) . join ( ', ' ) } ` : '' ,
mem . backlinks . length ? ` **Referenced by:** ${ mem . backlinks . map ( l = > ` [[ ${ l } ]] ` ) . join ( ', ' ) } ` : '' ,
'' ,
mem . content ,
] . filter ( l = > l !== undefined ) . join ( '\n' ) ;
}
private listNodes ( memories : Memory [ ] ) : MemoryRef [ ] {
private listNodes ( memories : Memory [ ] ) : MemoryRef [ ] {
return memories . map ( m = > ( { name : m.name , description : m.description } ) ) ;
return memories . map ( m = > ( { name : m.name , description : m.description } ) ) ;
}
}
@@ -197,6 +320,7 @@ export class MemoryManager {
async recollect ( query : string , memories : Memory [ ] | MemoryCache , limit = 5 , graphDepth = 1 ) : Promise < Memory [ ] > {
async recollect ( query : string , memories : Memory [ ] | MemoryCache , limit = 5 , graphDepth = 1 ) : Promise < Memory [ ] > {
const mem : Memory [ ] = memories instanceof MemoryCache ? memories.memories : memories ;
const mem : Memory [ ] = memories instanceof MemoryCache ? memories.memories : memories ;
if ( ! mem . length ) return [ ] ;
if ( ! mem . length ) return [ ] ;
const [ e ] = await this . llm . embedding ( query ) ;
const [ e ] = await this . llm . embedding ( query ) ;
if ( ! e ) return [ ] ;
if ( ! e ) return [ ] ;
@@ -212,7 +336,8 @@ export class MemoryManager {
for ( const name of frontier ) {
for ( const name of frontier ) {
const node = mem . find ( m = > m . name === name ) ;
const node = mem . find ( m = > m . name === name ) ;
if ( ! node ) continue ;
if ( ! node ) continue ;
for ( const link of node . links ) {
const { links } = extractMetadata ( node . content ) ;
for ( const link of links ) {
if ( ! found . has ( link ) && mem . find ( m = > m . name === link ) ) {
if ( ! found . has ( link ) && mem . find ( m = > m . name === link ) ) {
found . add ( link ) ;
found . add ( link ) ;
next . push ( link ) ;
next . push ( link ) ;
@@ -230,98 +355,146 @@ export class MemoryManager {
return ordered . map ( n = > mem . find ( m = > m . name === n ) ! ) . filter ( Boolean ) ;
return ordered . map ( n = > mem . find ( m = > m . name === n ) ! ) . filter ( Boolean ) ;
}
}
async memorize ( history : LLMMessage [ ] , memories : Memory [ ] | MemoryCache , options : LLMRequest ) : Promise < void > {
async memorize ( history : LLMMessage [ ] , memories : Memory [ ] | MemoryCache , options : LLMRequest ) : Promise < Memory [ ] > {
const mem = memories instanceof MemoryCache ? memories.memories : memories ;
const conversation = history
const conversation = history
. filter ( h = > h . role === 'user' || h . role === 'assistant' )
. filter ( h = > h . role === 'user' || h . role === 'assistant' )
. map ( h = > ` [ ${ h . role } ]: ${ h . content } ` ) . join ( '\n\n' ) . trim ( ) ;
. map ( h = > ` [ ${ h . role } ]: ${ h . content } ` ) . join ( '\n\n' ) . trim ( ) ;
if ( ! conversation ) return [ ] ;
if ( conversation ) {
// Create and insert temp memory immediately
const buckets = await this . factAgent ( conversation , mem , options ) ;
const trackingId = ` ${ Date . now ( ) } _ ${ Math . random ( ) } ` ;
if ( buckets . length ) {
const tempMemory = await this . createTempMemory ( conversation ) ;
await Promise . all ( buckets . map ( async bucket = > {
const mem = memories instanceof MemoryCache ? memories.memories : memories ;
const node = await this . organizingAgent ( bucket , mem , options ) ;
mem . push ( tempMemory ) ;
if ( ! mem. find ( m = > m . name === node . nam e ) ) mem. push ( node ) ;
if ( memories instanceof MemoryCach e ) memories . rebuild ( ) ;
await this . docAgent ( node , bucket , mem , options ) ;
this . pendingMemorizations . set ( trackingId , {
} ) ) ;
memories ,
}
tempMemoryName : tempMemory.name ,
}
timestamp : Date.now ( ) ,
// Auto-compress old journals
const weekAgo = Date . now ( ) - ( 7 * 24 * 60 * 60 * 1000 ) ;
const oldDailies = mem . filter ( m = > {
const journal = /^Journal\/(\d{4}-\d{2}-\d{2}$)/ . exec ( m . name ) ;
return journal && new Date ( journal [ 1 ] ) . getTime ( ) < weekAgo ;
} ) ;
} ) ;
if ( oldDailies . length ) {
try {
const byMonth = new Map < string , M emory [ ] > ( ) ;
await this . _memorizeBackground ( conversation , m emories , options ) ;
for ( const daily of oldDailies ) {
// Return the final memories (excluding temp ones)
const match = daily . name . match ( /^Journal\/(\d{4}-\d{2})-\d{2}$/ ) ;
const finalMem = memories instanceof MemoryCache ? memories.memories : memories ;
if ( ! match ) continue ;
return finalMem . filter ( m = > ! m . name . startsWith ( '_temp_' ) ) ;
const monthKey = match [ 1 ] ;
} catch ( err ) {
if ( ! byMonth . has ( monthKey ) ) byMonth . set ( monthKey , [ ] ) ;
throw err ;
byMonth . get ( monthKey ) ! . push ( daily ) ;
} finally {
// Remove temp memory from the exact same memory array/cache
const pending = this . pendingMemorizations . get ( trackingId ) ;
if ( pending ) {
const cleanMem = pending . memories instanceof MemoryCache
? pending.memories.memories
: pending.memories ;
const idx = cleanMem . findIndex ( m = > m . name === pending . tempMemoryName ) ;
if ( idx !== - 1 ) {
cleanMem . splice ( idx , 1 ) ;
}
if ( pending . memories instanceof MemoryCache ) {
pending . memories . rebuild ( ) ;
}
}
this . pendingMemorizations . delete ( trackingId ) ;
}
}
}
for ( const [ monthKey , ent ries] of byMonth ) {
private async _memorizeBackground ( conversation : string , memo ries: Memory [ ] | MemoryCache , options : LLMRequest ) : Promise < void > {
const monthlyPath = ` Journal/ ${ monthKey } ` ;
const mem = memories instanceof MemoryCache ? memories.memories : memories ;
le t monthl y = mem . find ( m = > m . name === monthlyPath ) ;
cons t monda y = getWeekMonday ( ) ;
if ( ! monthl y ) {
const sunday = getWeekSunday ( monda y ) ;
monthly = this . createNode ( m onthlyPath , mem ) ;
const buckets = await this . factAgent ( c onversation , mem , options , monday ) ;
mem . push ( monthly ) ;
if ( ! buckets . length ) return ;
}
const bucket : FactBucket = {
const runDocAgent = ( node : Memory , bucket : FactBucket , embedding? : number [ ] , week ? : { monday : string , sunday : string } ) = > {
subject : monthlyPath ,
if ( memories instanceof MemoryCache ) {
facts : entries.flatMap ( e = > e . content . split ( '\n' ) . filter ( line = > line . trim ( ) ) ) ,
return memories . lock ( node . name , ( ) = > this . docAgent ( node , bucket , mem , options , embedding , week ) ) ;
}
return this . docAgent ( node , bucket , mem , options , embedding , week ) ;
} ;
} ;
await th is. docAgent ( monthly , bucket , mem , options ) ;
await Prom ise . all ( buckets . map ( async bucket = > {
for ( const daily of entries ) {
let node = mem . find ( m = > m . name === bucket . subject && ! m . name . startsWith ( '_temp_' ) ) ;
const idx = mem . indexOf ( daily ) ;
let embedding : number [ ] | undefined ;
if ( idx !== - 1 ) mem . splice ( idx , 1 ) ;
}
if ( ! node || bucket . isNew ) {
const [ e ] = await this . llm . embedding ( ` ${ bucket . subject } \ n ${ bucket . facts . join ( '\n' ) } ` ) ;
embedding = e ? . embedding ;
if ( ! node ) {
node = this . createNode ( bucket . subject , mem ) ;
mem . push ( node ) ;
}
}
}
}
if ( memories instanceof MemoryCache ) {
const week = bucket . subject . startsWith ( 'Journal/' ) ? { monday , sunday } : undefined ;
memories . rebuildLinks ( ) ;
await runDocAgent ( node , bucket , embedding , week ) ;
} ) ) ;
if ( memories instanceof MemoryCache )
memories . rebuild ( ) ;
memories . rebuild ( ) ;
} else {
rebuildBacklinks ( mem ) ;
}
}
}
private async docAgent ( node : Memory , bucket : FactBucket , memories : Memory [ ] , options : LLMRequest ) : Promise < void > {
private buildHeader ( node : Memory , week ? : { monday : string , sunday : string } , links : string [ ] = [ ] , backlinks : string [ ] = [ ] ) : string {
const tags = node . name . split ( '/' ) [ 0 ] ? . toLowerCase ( ) ;
const lines = [
'---' ,
` name: ${ node . name } ` ,
` description: ${ node . description || '' } ` ,
tags ? ` tags: [ ${ tags } ] ` : '' ,
links . length ? ` links: [ ${ links . map ( l = > ` " ${ l } " ` ) . join ( ', ' ) } ] ` : 'links: []' ,
backlinks . length ? ` backlinks: [ ${ backlinks . map ( l = > ` " ${ l } " ` ) . join ( ', ' ) } ] ` : 'backlinks: []' ,
week ? ` week: ${ week . monday } – ${ week . sunday } ` : '' ,
` modified: ${ new Date ( ) . toISOString ( ) } ` ,
'---' ,
] . filter ( Boolean ) ;
return lines . join ( '\n' ) ;
}
private applyHeader ( content : string , header : string ) : string {
const hasFrontmatter = content . trimStart ( ) . startsWith ( '---' ) ;
if ( hasFrontmatter ) {
return content . replace ( /^---[\s\S]*?---\n?/ , ` ${ header } \ n ` ) ;
}
return ` ${ header } \ n \ n ${ content } ` ;
}
private updateFrontmatter ( content : string , updates : { links? : string [ ] , backlinks? : string [ ] } ) : string {
const match = content . match ( /^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/ ) ;
if ( ! match ) return content ;
const [ , fm , body ] = match ;
let newFm = fm ;
if ( updates . links !== undefined ) {
const linksList = updates . links . length ? ` [ ${ updates . links . map ( l = > ` " ${ l } " ` ) . join ( ', ' ) } ] ` : '[]' ;
newFm = newFm . replace ( /^links:.*$/m , ` links: ${ linksList } ` ) ;
}
if ( updates . backlinks !== undefined ) {
const backlinksList = updates . backlinks . length ? ` [ ${ updates . backlinks . map ( l = > ` " ${ l } " ` ) . join ( ', ' ) } ] ` : '[]' ;
newFm = newFm . replace ( /^backlinks:.*$/m , ` backlinks: ${ backlinksList } ` ) ;
}
newFm = newFm . replace ( /^modified:.*$/m , ` modified: ${ new Date ( ) . toISOString ( ) } ` ) ;
return ` --- \ n ${ newFm } \ n--- \ n \ n ${ body } ` ;
}
private stripHeader ( content : string ) : string {
return content . replace ( /^---[\s\S]*?---\n?/ , '' ) . trimStart ( ) ;
}
private async docAgent ( node : Memory , bucket : FactBucket , memories : Memory [ ] , options : LLMRequest , precomputedEmbedding? : number [ ] , week ? : { monday : string , sunday : string } ) : Promise < void > {
const { links : oldLinks } = extractMetadata ( node . content ) ;
let finalContent = node . content ;
let finalContent = node . content ;
const isJournalCompression = node . name . match ( /^Journal\/\d{4}-\d{2}$/ ) ;
const systemPrompt = isJournalCompression
? ` You are a journal compressor. Condense the daily entries below into a monthly summary.
Format:
await this . llm . ask (
# ${ node . name }
` New facts to integrate: \ n ${ bucket . facts . map ( f = > ` - ${ f } ` ) . join ( '\n' ) } ` ,
{
## Themes
model : options.model ,
(Recurring topics, moods, patterns)
temperature : 0.3 ,
system : ` You are a knowledge base editor. Integrate the provided facts into the document below.
## Key Events
(Important moments, decisions, milestones)
## Notable Conversations
(Significant discussions or revelations)
Rules:
- Use [[WikiLinks]] to reference permanent notes using full paths like [[People/Sarah]] or [[Projects/Website]]
- Keep it concise but preserve emotional/temporal context
- Discard filler but keep things the user vented about or cared about
- If a fact belongs in a permanent note, link to it instead of duplicating
Current monthly summary:
\` \` \` markdown
${ node . content || '(empty — first compression for this month)' }
\` \` \` `
: ` You are a knowledge base editor. Integrate the provided facts into the document below.
Formatting rules:
Formatting rules:
- Use Obsidian-style markdown: # headings, **bold** for key terms, bullet lists for facts
- Use Obsidian-style markdown: # headings, **bold** for key terms, bullet lists for facts
@@ -330,50 +503,75 @@ Formatting rules:
- Keep the document concise, factual, and human-readable
- Keep the document concise, factual, and human-readable
- Resolve any contradictions between old content and new facts (new facts win)
- Resolve any contradictions between old content and new facts (new facts win)
- Do not add filler, preamble, or AI commentary — just clean knowledge documents
- Do not add filler, preamble, or AI commentary — just clean knowledge documents
- The document begins with a YAML frontmatter block (between --- markers) — do not remove or rewrite it, it is maintained automatically
${ week ? '- This is a weekly journal entry. The frontmatter contains the week date range.\n' : '' }
All nodes:
All nodes:
${ this . listNodes ( memories ) . map ( n = > n . name ) . join ( ', ' ) || 'none' }
${ this . listNodes ( memories ) . map ( n = > n . name ) . join ( ', ' ) || 'none' }
Current document:
Current document:
\` \` \` markdown
\` \` \` markdown
${ node . content || '(empty — this is a new document)' }
${ node . content || '(empty — this is a new document)' }
\` \` \` ` ;
\` \` \` ` ,
await this . llm . ask (
` New facts to integrate: \ n ${ bucket . facts . map ( f = > ` - ${ f } ` ) . join ( '\n' ) } ` ,
{
model : options.model ,
temperature : 0.3 ,
system : systemPrompt ,
tools : [ {
tools : [ {
name : 'update_document' ,
name : 'update_document' ,
description : 'Write the complete updated document content' ,
description : 'Write the complete updated document content. Include everything after the frontmatter block — the frontmatter will be recalculated automatically. ' ,
args : {
args : {
description : { type : 'string' , description : 'One-line description of what this document covers, no formatting or emojis' , required : true } ,
description : { type : 'string' , description : 'One-line description of what this document covers, no formatting or emojis' , required : true } ,
content : { type : 'string' , description : 'Fully updated d ocument in markdown' , required : true } ,
content : { type : 'string' , description : 'D ocument body in markdown, without the frontmatter block ' , required : true } ,
} ,
} ,
fn : ( args : any ) = > {
fn : ( args : any ) = > {
node . description = args . description ;
node . description = args . description ;
finalContent = args . content ;
finalContent = args . content ;
return 'Saved' ;
return 'Saved' ;
}
} ,
} ]
} ] ,
}
}
) ;
) ;
node . content = finalContent ;
const newLinks = extractLinks ( finalContent ) . filter ( l = > l !== node . name ) ;
node . links = extractLinks ( finalContent ) ;
const newLinkSet = new Set ( newLinks ) ;
const needsEmbed = ! node . embedding ? . length || node . description !== memories . find ( m = > m . name === node . name ) ? . description ;
const oldLinkSet = new Set ( oldLinks ) ;
if ( needsEmbed ) {
const [ e ] = await this . llm . embedding ( node . description ) ;
for ( const added of newLinkSet ) {
if ( ! oldLinkSet . has ( added ) ) {
const target = memories . find ( m = > m . name === added ) ;
if ( target ) {
const { backlinks } = extractMetadata ( target . content ) ;
if ( ! backlinks . includes ( node . name ) ) {
target . content = this . updateFrontmatter ( target . content , {
backlinks : [ . . . backlinks , node . name ] ,
} ) ;
}
}
}
}
for ( const removed of oldLinkSet ) {
if ( ! newLinkSet . has ( removed ) ) {
const target = memories . find ( m = > m . name === removed ) ;
if ( target ) {
const { backlinks } = extractMetadata ( target . content ) ;
target . content = this . updateFrontmatter ( target . content , {
backlinks : backlinks.filter ( b = > b !== node . name ) ,
} ) ;
}
}
}
const { backlinks } = extractMetadata ( node . content ) ;
const header = this . buildHeader ( node , week , newLinks , backlinks ) ;
node . content = this . applyHeader ( finalContent , header ) ;
if ( precomputedEmbedding ) {
node . embedding = precomputedEmbedding ;
} else {
const embedInput = ` ${ node . description } \ n \ n ${ this . stripHeader ( node . content ) } ` . trim ( ) ;
const [ e ] = await this . llm . embedding ( embedInput ) ;
if ( e ) node . embedding = e . embedding ;
if ( e ) node . embedding = e . embedding ;
}
}
}
}
private async factAgent ( conversation : string , memories : Memory [ ] , options : LLMRequest ) : Promise < FactBucket [ ] > {
private async factAgent ( conversation : string , memories : Memory [ ] , options : LLMRequest , weekKey : string ): Promise < FactBucket [ ] > {
const buckets : FactBucket [ ] = [ ] ;
const buckets : FactBucket [ ] = [ ] ;
const today = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
await this . llm . ask ( conversation , {
await this . llm . ask ( conversation , {
model : options.model ,
model : options.model ,
temperature : 0.2 ,
temperature : 0.2 ,
@@ -386,116 +584,35 @@ Rules:
- DO NOT extract greetings, pleasantries, or generic exchanges
- DO NOT extract greetings, pleasantries, or generic exchanges
- If nothing worth remembering was said, do not call any tools
- If nothing worth remembering was said, do not call any tools
**Organiz ational patterns:**
When extracting facts, you MUST also decide the exact destin ation path:
- Journal entries use paths like: Journal/ ${ today }
- Use an existing node name if the facts clearly belong there
- People use paths like: People/Name
- All information primary about the user should go under "Personal" (e.g., Personal/Goals, Personal/Habits)
- Projects use paths like: Projects/Name
- When required, create a new path following collection/subject format (e.g., People/Sarah, Projects/Oxide)
- Personal info uses paths like: Personal/Goals, Personal/Tasks, etc.
- For journal entries, use "journal" (will auto-route to Journal/ ${ weekKey } )
- General knowledge uses paths like: Biology/Topic, History/Topic, etc.
Learn from existing nodes and follow the same pattern when extracting.
Available nodes:
Group facts by subject. For each group call \` extract_facts \` once with the FULL PATH.
Known nodes (name: description):
${ this . listNodes ( memories ) . map ( n = > ` - ${ n . name } : ${ n . description } ` ) . join ( '\n' ) || 'None yet.' } ` ,
${ this . listNodes ( memories ) . map ( n = > ` - ${ n . name } : ${ n . description } ` ) . join ( '\n' ) || 'None yet.' } ` ,
tools : [ {
tools : [ {
name : 'extract_facts' ,
name : 'extract_facts' ,
description : 'Submit a group of related facts for a specific subject ' ,
description : 'Submit facts with their destination ' ,
args : {
args : {
subject : { type : 'string' , description : 'Full path for the subject (e.g., "Journal/2025-01-27", "People/Sarah", "Projects/Websit e")' , required : true } ,
destination : { type : 'string' , description : 'Exact existing node name OR new path (e.g. "People/Sarah", "Projects/Oxid e")' , required : true } ,
facts : { type : 'string' , description : 'Comma-separated list of extracted facts' , required : true } ,
facts : { type : 'string' , description : 'Comma-separated facts' , required : true } ,
create_new : { type : 'boolean' , description : 'True if this is a new node that doesn\'t exist yet' , required : true } ,
} ,
} ,
fn : ( args : any ) = > {
fn : ( args : any ) = > {
const subject = args . destination . trim ( ) . toLowerCase ( ) === 'journal'
? ` Journal/ ${ weekKey } `
: args . destination ;
buckets . push ( {
buckets . push ( {
subject : args.subject ,
subject ,
facts : args.facts.split ( ',' ) . map ( ( f : string ) = > f . trim ( ) ) . filter ( Boolean ) ,
facts : args.facts.split ( ',' ) . map ( ( f : string ) = > f . trim ( ) ) . filter ( Boolean ) ,
isNew : args.create_new ,
} ) ;
} ) ;
return 'Recorded' ;
return 'Recorded' ;
}
} ,
} ]
} ] ,
} ) ;
} ) ;
return buckets ;
return buckets ;
}
}
private async organizingAgent ( bucket : FactBucket , memories : Memory [ ] , options : LLMRequest ) : Promise < Memory > {
let candidates = this . listNodes ( memories ) ;
let attempts = 0 ;
const maxAttempts = 3 ;
while ( attempts ++ < maxAttempts ) {
let home = '' , mode : string | null = null ;
const resp = await this . llm . ask ( ` Subject: ${ bucket . subject } \ n \ nFacts: \ n ${ bucket . facts . map ( f = > ` - ${ f } ` ) . join ( '\n' ) } ` , {
model : options.model ,
temperature : 0.1 ,
system : ` You are a knowledge organizer. Your job is to find the correct home for the supplied facts.
1. Review the facts and the node list below. Pick the most likely match or decide if a new node is needed.
2. If you picked an existing node, use \` read \` to verify it's the right place.
- After reading, call either \` confirm \` (correct node) or \` mismatched \` (wrong node).
3. If none of the nodes match, call \` create \` to make a new node.
**Organizational patterns:**
- Journal entries: Journal/YYYY-MM-DD
- People: People/Name
- Projects: Projects/Name
- Personal: Personal/Goals, Personal/Tasks, etc.
- Knowledge: Biology/Topic, History/Topic, etc.
Available nodes:
${ candidates . map ( n = > ` - ${ n . name } : ${ n . description } ` ) . join ( '\n' ) || 'None — create a new node.' } ` ,
tools : [ {
name : 'read' ,
description : 'Read a node file to verify it is the right home for these facts' ,
args : { name : { type : 'string' , description : 'Exact node name (full path)' , required : true } } ,
fn : ( { name } ) = > {
const mem = memories . find ( m = > m . name === name ) ;
if ( ! mem ) return 'Node not found' ;
home = name ;
return this . formatMemory ( mem ) ;
}
} , {
name : 'confirm' ,
description : 'Confirm this is the correct node for the facts' ,
args : { } ,
fn : ( ) = > {
mode = 'success' ;
resp . abort ( ) ;
}
} , {
name : 'mismatched' ,
description : 'This is not the node you are looking for' ,
args : { } ,
fn : ( ) = > {
mode = 'failed' ;
resp . abort ( ) ;
}
} , {
name : 'create' ,
description : 'No existing node fits — create a new one' ,
args : {
name : { type : 'string' , description : 'Full path for the new node (e.g., "People/Sarah", "Journal/2025-01-27")' , required : true }
} ,
fn : ( { name } ) = > {
home = name ;
mode = 'create' ;
resp . abort ( ) ;
}
} ]
} ) ;
if ( mode === 'create' ) {
return this . createNode ( home , memories ) ;
} else if ( mode === 'failed' ) {
candidates = candidates . filter ( c = > c . name !== home ) ;
if ( ! candidates . length ) return this . createNode ( bucket . subject , memories ) ;
} else if ( mode === 'success' ) {
const existing = memories . find ( m = > m . name === home ) ;
return existing || this . createNode ( home , memories ) ;
}
}
return this . createNode ( bucket . subject , memories ) ;
}
}
}