@onmars/lunar-core 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onmars/lunar-core",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -42,7 +42,7 @@ export interface HookContext {
42
42
  /** Current session message count */
43
43
  messageCount: number
44
44
  /** Session messages (for summarize, promote) */
45
- messages?: Array<{ role: string; content: string }>
45
+ messages?: Array<{ role: string; content: string; timestamp?: string }>
46
46
  /** Recall results — mutable (afterRecall boost modifies scores) */
47
47
  results?: MemoryResult[]
48
48
  /** Search query — mutable (beforeRecall can expand/modify) */
@@ -151,6 +151,55 @@ export class MemoryOrchestrator implements MemoryProvider {
151
151
  return this.diaryResults.length
152
152
  }
153
153
 
154
+ /**
155
+ * Refresh episode for a new session (called after /clear).
156
+ * Creates a new episode with today's date, replacing the stale one.
157
+ */
158
+ async refreshEpisode(channel?: string): Promise<void> {
159
+ if (!this.hookRunner?.hasHooks('sessionStart')) return
160
+
161
+ const context: HookContext = {
162
+ messageCount: 0,
163
+ channel,
164
+ sessionDate: new Date().toISOString().split('T')[0],
165
+ }
166
+ const result = await this.hookRunner.run('sessionStart', context)
167
+ if (result.episodeId) {
168
+ this.currentEpisodeId = result.episodeId
169
+ this.currentSessionDate = result.sessionDate
170
+ log.info(
171
+ { episodeId: result.episodeId, sessionDate: result.sessionDate },
172
+ 'Episode refreshed after clear',
173
+ )
174
+ }
175
+ if (result.results?.length) {
176
+ this.diaryResults = result.results
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get or create an episode for a specific date (used by day-split in /clear).
182
+ * Does NOT change the current orchestrator state.
183
+ */
184
+ async getEpisodeForDate(date: string, channel?: string): Promise<string | undefined> {
185
+ // Find a provider that supports getOrCreateEpisode (Brain provider)
186
+ const brain = [...this.providersByName.values()].find(
187
+ (p) => (p as any).getOrCreateEpisode !== undefined,
188
+ ) as any
189
+ if (!brain?.getOrCreateEpisode) return undefined
190
+
191
+ try {
192
+ const episode = await brain.getOrCreateEpisode(date, channel)
193
+ return episode.id
194
+ } catch (err) {
195
+ log.warn(
196
+ { error: err instanceof Error ? err.message : String(err), date },
197
+ 'Failed to get episode for date',
198
+ )
199
+ return undefined
200
+ }
201
+ }
202
+
154
203
  /**
155
204
  * Destroy all providers.
156
205
  */
@@ -291,7 +340,7 @@ export class MemoryOrchestrator implements MemoryProvider {
291
340
  * 3. afterSessionEnd hooks (promote → Engram facts → Brain)
292
341
  */
293
342
  async onSessionEnd(context: {
294
- messages: Array<{ role: string; content: string }>
343
+ messages: Array<{ role: string; content: string; timestamp?: string }>
295
344
  sessionSummary?: string
296
345
  topics?: string[]
297
346
  episodeId?: string
package/src/lib/router.ts CHANGED
@@ -60,7 +60,7 @@ export class Router {
60
60
  * In-memory message history per session — used for graceful session close.
61
61
  * Capped at MAX_SESSION_HISTORY messages per session to bound memory usage.
62
62
  */
63
- private sessionMessages = new Map<string, Array<{ role: string; content: string }>>()
63
+ private sessionMessages = new Map<string, Array<{ role: string; content: string; timestamp?: string }>>()
64
64
  private static readonly MAX_SESSION_HISTORY = 200
65
65
 
66
66
  /** Tracks cumulative token usage per session */
@@ -582,18 +582,53 @@ export class Router {
582
582
  if (messages.length > 0) {
583
583
  try {
584
584
  const orchestrator = this.memory as unknown as MemoryOrchestrator
585
- const clearContext: Record<string, unknown> = {
586
- messages,
587
- topics: this.extractTopics(messages),
585
+
586
+ // Group messages by day (using timestamp if available, else today)
587
+ const today = new Date().toISOString().split('T')[0]
588
+ const messagesByDay = new Map<string, typeof messages>()
589
+
590
+ for (const msg of messages) {
591
+ const day = msg.timestamp ? msg.timestamp.split('T')[0] : today
592
+ if (!messagesByDay.has(day)) messagesByDay.set(day, [])
593
+ messagesByDay.get(day)!.push(msg)
594
+ }
595
+
596
+ // Sort days chronologically
597
+ const sortedDays = [...messagesByDay.keys()].sort()
598
+
599
+ // Extract channel from sessionKey (format: agentId:platform:channelId)
600
+ const channel = sessionKey.split(':').slice(1).join(':')
601
+
602
+ // Run hooks for each day's messages
603
+ for (const day of sortedDays) {
604
+ const dayMessages = messagesByDay.get(day)!
605
+ const clearContext: Record<string, unknown> = {
606
+ messages: dayMessages,
607
+ topics: this.extractTopics(dayMessages),
608
+ sessionDate: day,
609
+ }
610
+
611
+ // Get or create episode for this specific day
612
+ if (orchestrator?.getEpisodeForDate) {
613
+ const episodeId = await orchestrator.getEpisodeForDate(day, channel)
614
+ if (episodeId) clearContext.episodeId = episodeId
615
+ }
616
+
617
+ await this.memory!.onSessionEnd(
618
+ clearContext as Parameters<NonNullable<MemoryProvider['onSessionEnd']>>[0],
619
+ )
588
620
  }
589
- if (orchestrator?.episodeId) clearContext.episodeId = orchestrator.episodeId
590
- if (orchestrator?.sessionDate) clearContext.sessionDate = orchestrator.sessionDate
591
621
 
592
- await this.memory.onSessionEnd(
593
- clearContext as Parameters<NonNullable<MemoryProvider['onSessionEnd']>>[0],
594
- )
595
622
  hooksRan = true
596
- log.info({ sessionKey, messageCount: messages.length }, 'Memory hooks ran before clear')
623
+ log.info(
624
+ { sessionKey, messageCount: messages.length, days: sortedDays.length },
625
+ 'Memory hooks ran before clear',
626
+ )
627
+
628
+ // Refresh episode for next session
629
+ if (orchestrator?.refreshEpisode) {
630
+ await orchestrator.refreshEpisode(channel)
631
+ }
597
632
  } catch (err) {
598
633
  const errMsg = err instanceof Error ? err.message : String(err)
599
634
  log.warn({ sessionKey, error: errMsg }, 'Memory hooks failed during /clear')
@@ -794,19 +829,48 @@ export class Router {
794
829
  closePromises.push(
795
830
  (async () => {
796
831
  try {
797
- // Pass episodeId from orchestrator if available
798
832
  const orchestrator = this.memory as unknown as MemoryOrchestrator
799
- const sessionContext: Record<string, unknown> = {
800
- messages,
801
- topics: this.extractTopics(messages),
833
+
834
+ // Group messages by day (using timestamp if available, else today)
835
+ const today = new Date().toISOString().split('T')[0]
836
+ const messagesByDay = new Map<string, typeof messages>()
837
+
838
+ for (const msg of messages) {
839
+ const day = msg.timestamp ? msg.timestamp.split('T')[0] : today
840
+ if (!messagesByDay.has(day)) messagesByDay.set(day, [])
841
+ messagesByDay.get(day)!.push(msg)
842
+ }
843
+
844
+ // Sort days chronologically
845
+ const sortedDays = [...messagesByDay.keys()].sort()
846
+
847
+ // Extract channel from sessionKey (format: agentId:platform:channelId)
848
+ const channel = sessionKey.split(':').slice(1).join(':')
849
+
850
+ // Run hooks for each day's messages
851
+ for (const day of sortedDays) {
852
+ const dayMessages = messagesByDay.get(day)!
853
+ const sessionContext: Record<string, unknown> = {
854
+ messages: dayMessages,
855
+ topics: this.extractTopics(dayMessages),
856
+ sessionDate: day,
857
+ }
858
+
859
+ // Get or create episode for this specific day
860
+ if (orchestrator?.getEpisodeForDate) {
861
+ const episodeId = await orchestrator.getEpisodeForDate(day, channel)
862
+ if (episodeId) sessionContext.episodeId = episodeId
863
+ }
864
+
865
+ await this.memory!.onSessionEnd!(
866
+ sessionContext as Parameters<NonNullable<MemoryProvider['onSessionEnd']>>[0],
867
+ )
802
868
  }
803
- if (orchestrator?.episodeId) sessionContext.episodeId = orchestrator.episodeId
804
- if (orchestrator?.sessionDate) sessionContext.sessionDate = orchestrator.sessionDate
805
869
 
806
- await this.memory!.onSessionEnd!(
807
- sessionContext as Parameters<NonNullable<MemoryProvider['onSessionEnd']>>[0],
870
+ log.info(
871
+ { sessionKey, messageCount: messages.length, days: sortedDays.length },
872
+ 'Session closed gracefully',
808
873
  )
809
- log.info({ sessionKey, messageCount: messages.length }, 'Session closed gracefully')
810
874
  } catch (err) {
811
875
  const errMsg = err instanceof Error ? err.message : String(err)
812
876
  log.warn(
@@ -849,7 +913,7 @@ export class Router {
849
913
  this.sessionMessages.set(sessionKey, history)
850
914
  }
851
915
 
852
- history.push({ role, content })
916
+ history.push({ role, content, timestamp: new Date().toISOString() })
853
917
 
854
918
  // Cap to prevent unbounded memory growth
855
919
  if (history.length > Router.MAX_SESSION_HISTORY) {
@@ -78,7 +78,7 @@ export interface MemoryProvider {
78
78
  * - Markdown: write daily note
79
79
  */
80
80
  onSessionEnd?(context: {
81
- messages: Array<{ role: string; content: string }>
81
+ messages: Array<{ role: string; content: string; timestamp?: string }>
82
82
  sessionSummary?: string
83
83
  topics?: string[]
84
84
  }): Promise<void>