@onmars/lunar-core 0.4.7 → 0.6.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 +1 -1
- package/src/lib/hook-runner.ts +207 -1
- package/src/lib/memory-orchestrator.ts +50 -1
- package/src/lib/router.ts +84 -20
- package/src/types/memory.ts +1 -1
package/package.json
CHANGED
package/src/lib/hook-runner.ts
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* minMessages — Minimum session message count
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import { execSync } from 'node:child_process'
|
|
22
25
|
import type { Fact, MemoryProvider, MemoryResult } from '../types/memory'
|
|
23
26
|
import type { HookCondition, HookEntry, HooksConfig, LifecycleEvent } from './config-loader'
|
|
24
27
|
import { log } from './logger'
|
|
@@ -42,7 +45,7 @@ export interface HookContext {
|
|
|
42
45
|
/** Current session message count */
|
|
43
46
|
messageCount: number
|
|
44
47
|
/** Session messages (for summarize, promote) */
|
|
45
|
-
messages?: Array<{ role: string; content: string }>
|
|
48
|
+
messages?: Array<{ role: string; content: string; timestamp?: string }>
|
|
46
49
|
/** Recall results — mutable (afterRecall boost modifies scores) */
|
|
47
50
|
results?: MemoryResult[]
|
|
48
51
|
/** Search query — mutable (beforeRecall can expand/modify) */
|
|
@@ -1129,6 +1132,206 @@ const executeDiaryRecall: ActionExecutor = async (hook, context, providers) => {
|
|
|
1129
1132
|
}
|
|
1130
1133
|
}
|
|
1131
1134
|
|
|
1135
|
+
// ════════════════════════════════════════════════════════════
|
|
1136
|
+
// sessionLog — Write session summary to memory/YYYY-MM-DD.md
|
|
1137
|
+
// ════════════════════════════════════════════════════════════
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* `sessionLog` — Write session details to a daily markdown file.
|
|
1141
|
+
*
|
|
1142
|
+
* Creates/appends to `~/.lunar/memory/YYYY-MM-DD.md` with session summary,
|
|
1143
|
+
* topics, channel, and message count. This serves as an offline backup
|
|
1144
|
+
* that's git-tracked and grep-searchable, independent of Brain availability.
|
|
1145
|
+
*
|
|
1146
|
+
* Fields: (none required — uses context)
|
|
1147
|
+
* Events: afterSessionEnd
|
|
1148
|
+
*/
|
|
1149
|
+
const executeSessionLog: ActionExecutor = async (_hook, context) => {
|
|
1150
|
+
if (!context.sessionSummary && (!context.messages || context.messages.length === 0)) {
|
|
1151
|
+
log.debug({ action: 'sessionLog' }, 'Hook sessionLog: no summary or messages — skipping')
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const sessionDate = context.sessionDate ?? new Date().toISOString().split('T')[0]
|
|
1156
|
+
const memoryDir = join(process.env.HOME ?? '/home/mars', '.lunar', 'memory')
|
|
1157
|
+
const filePath = join(memoryDir, `${sessionDate}.md`)
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
// Ensure memory directory exists
|
|
1161
|
+
if (!existsSync(memoryDir)) {
|
|
1162
|
+
mkdirSync(memoryDir, { recursive: true })
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const now = new Date()
|
|
1166
|
+
const timeStr = now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })
|
|
1167
|
+
const channel = context.channel ?? 'unknown'
|
|
1168
|
+
|
|
1169
|
+
// Build session section
|
|
1170
|
+
const lines: string[] = []
|
|
1171
|
+
|
|
1172
|
+
// Add header if file is new
|
|
1173
|
+
if (!existsSync(filePath)) {
|
|
1174
|
+
lines.push(`# Session Log — ${sessionDate}`)
|
|
1175
|
+
lines.push('')
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
lines.push(`## ${timeStr} — #${channel} (${context.messageCount} msgs)`)
|
|
1179
|
+
lines.push('')
|
|
1180
|
+
|
|
1181
|
+
if (context.topics?.length) {
|
|
1182
|
+
lines.push(`**Topics:** ${context.topics.join(', ')}`)
|
|
1183
|
+
lines.push('')
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (context.sessionSummary) {
|
|
1187
|
+
lines.push('### Summary')
|
|
1188
|
+
lines.push('')
|
|
1189
|
+
lines.push(context.sessionSummary)
|
|
1190
|
+
lines.push('')
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (context.episodeId) {
|
|
1194
|
+
lines.push(`*Episode: ${context.episodeId.slice(0, 8)}*`)
|
|
1195
|
+
lines.push('')
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
lines.push('---')
|
|
1199
|
+
lines.push('')
|
|
1200
|
+
|
|
1201
|
+
const content = lines.join('\n')
|
|
1202
|
+
|
|
1203
|
+
// Append to existing file or create new
|
|
1204
|
+
if (existsSync(filePath)) {
|
|
1205
|
+
appendFileSync(filePath, content, 'utf-8')
|
|
1206
|
+
} else {
|
|
1207
|
+
writeFileSync(filePath, content, 'utf-8')
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
log.info(
|
|
1211
|
+
{ file: filePath, channel, messageCount: context.messageCount },
|
|
1212
|
+
'Hook sessionLog: written',
|
|
1213
|
+
)
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
1216
|
+
log.warn({ error: errMsg, file: filePath }, 'Hook sessionLog: failed — continuing')
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// ════════════════════════════════════════════════════════════
|
|
1221
|
+
// pipelineExtract — Extract entities/facts via Brain pipeline
|
|
1222
|
+
// ════════════════════════════════════════════════════════════
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* `pipelineExtract` — Call Brain's pipeline/extract endpoint to automatically
|
|
1226
|
+
* extract entities, facts, and relations from the conversation transcript.
|
|
1227
|
+
*
|
|
1228
|
+
* Fields: provider (brain provider with extractPipeline support)
|
|
1229
|
+
* Events: afterSessionEnd
|
|
1230
|
+
*/
|
|
1231
|
+
const executePipelineExtract: ActionExecutor = async (hook, context, providers) => {
|
|
1232
|
+
const { provider: providerName } = hook
|
|
1233
|
+
|
|
1234
|
+
if (!providerName) {
|
|
1235
|
+
log.warn({ action: 'pipelineExtract' }, 'Hook pipelineExtract: requires "provider" — skipping')
|
|
1236
|
+
return
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (!context.messages || context.messages.length < 4) {
|
|
1240
|
+
log.debug(
|
|
1241
|
+
{ action: 'pipelineExtract', messageCount: context.messages?.length ?? 0 },
|
|
1242
|
+
'Hook pipelineExtract: too few messages — skipping',
|
|
1243
|
+
)
|
|
1244
|
+
return
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const provider = providers.get(providerName)
|
|
1248
|
+
if (!provider) {
|
|
1249
|
+
log.warn(
|
|
1250
|
+
{ provider: providerName, action: 'pipelineExtract' },
|
|
1251
|
+
'Hook pipelineExtract: provider not found — skipping',
|
|
1252
|
+
)
|
|
1253
|
+
return
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Access brain-specific extractPipeline method
|
|
1257
|
+
const brain = provider as unknown as {
|
|
1258
|
+
extractPipeline?: (conversation: string, channel?: string, sessionDate?: string) => Promise<unknown>
|
|
1259
|
+
}
|
|
1260
|
+
if (!brain.extractPipeline) {
|
|
1261
|
+
log.warn(
|
|
1262
|
+
{ action: 'pipelineExtract' },
|
|
1263
|
+
'Hook pipelineExtract: provider does not support extractPipeline — skipping',
|
|
1264
|
+
)
|
|
1265
|
+
return
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
const transcript = smartTruncate(
|
|
1270
|
+
context.messages.map((m) => `${m.role}: ${m.content}`).join('\n'),
|
|
1271
|
+
MAX_TRANSCRIPT_CHARS,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
const sessionDate = context.sessionDate ?? new Date().toISOString().split('T')[0]
|
|
1275
|
+
|
|
1276
|
+
await brain.extractPipeline(transcript, context.channel ?? 'unknown', sessionDate)
|
|
1277
|
+
|
|
1278
|
+
log.info(
|
|
1279
|
+
{ provider: providerName, chars: transcript.length },
|
|
1280
|
+
'Hook pipelineExtract: complete',
|
|
1281
|
+
)
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
1284
|
+
log.warn({ error: errMsg }, 'Hook pipelineExtract: failed — continuing')
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// ════════════════════════════════════════════════════════════
|
|
1289
|
+
// gitSync — Commit and push workspace changes
|
|
1290
|
+
// ════════════════════════════════════════════════════════════
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* `gitSync` — Commit and push any workspace changes after session end.
|
|
1294
|
+
*
|
|
1295
|
+
* Runs: git add -A && git commit && git push in the workspace directory.
|
|
1296
|
+
* Best-effort: failures are logged but never block the flow.
|
|
1297
|
+
*
|
|
1298
|
+
* Fields: (none — uses ~/.lunar as workspace)
|
|
1299
|
+
* Events: afterSessionEnd (should be LAST hook)
|
|
1300
|
+
*/
|
|
1301
|
+
const executeGitSync: ActionExecutor = async () => {
|
|
1302
|
+
const workspaceDir = join(process.env.HOME ?? '/home/mars', '.lunar')
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
// Check if there are changes to commit
|
|
1306
|
+
const status = execSync('git status --porcelain', {
|
|
1307
|
+
cwd: workspaceDir,
|
|
1308
|
+
encoding: 'utf-8',
|
|
1309
|
+
timeout: 10_000,
|
|
1310
|
+
}).trim()
|
|
1311
|
+
|
|
1312
|
+
if (!status) {
|
|
1313
|
+
log.debug({ action: 'gitSync' }, 'Hook gitSync: no changes — skipping')
|
|
1314
|
+
return
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const today = new Date().toISOString().split('T')[0]
|
|
1318
|
+
const commitMsg = `session log ${today} — auto-sync`
|
|
1319
|
+
|
|
1320
|
+
execSync('git add -A', { cwd: workspaceDir, timeout: 10_000 })
|
|
1321
|
+
execSync(`git commit -m "${commitMsg}"`, {
|
|
1322
|
+
cwd: workspaceDir,
|
|
1323
|
+
timeout: 15_000,
|
|
1324
|
+
})
|
|
1325
|
+
execSync('git push', { cwd: workspaceDir, timeout: 30_000 })
|
|
1326
|
+
|
|
1327
|
+
log.info({ workspace: workspaceDir }, 'Hook gitSync: committed and pushed')
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
1330
|
+
// Git sync failure is non-critical — don't block
|
|
1331
|
+
log.warn({ error: errMsg }, 'Hook gitSync: failed — continuing')
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1132
1335
|
const ACTION_REGISTRY = new Map<string, ActionExecutor>([
|
|
1133
1336
|
['promote', executePromote],
|
|
1134
1337
|
['boost', executeBoost],
|
|
@@ -1140,6 +1343,9 @@ const ACTION_REGISTRY = new Map<string, ActionExecutor>([
|
|
|
1140
1343
|
['reconcile', executeReconcile],
|
|
1141
1344
|
['episodeClose', executeEpisodeClose],
|
|
1142
1345
|
['diaryRecall', executeDiaryRecall],
|
|
1346
|
+
['sessionLog', executeSessionLog],
|
|
1347
|
+
['pipelineExtract', executePipelineExtract],
|
|
1348
|
+
['gitSync', executeGitSync],
|
|
1143
1349
|
])
|
|
1144
1350
|
|
|
1145
1351
|
/**
|
|
@@ -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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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(
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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) {
|
package/src/types/memory.ts
CHANGED
|
@@ -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>
|