@rlabs-inc/memory 0.4.12 → 0.4.13

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": "@rlabs-inc/memory",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -303,11 +303,11 @@ export async function ingest(options: IngestOptions) {
303
303
  const segments = await parseSessionFileWithSegments(session.filepath, maxTokens)
304
304
  totalSegments += segments.length
305
305
 
306
- // Accumulate all memories from this session for manager
306
+ // Accumulate all results from this session for manager
307
307
  const sessionMemories: CuratedMemory[] = []
308
- let sessionSummary = ''
309
- let interactionTone = ''
310
- let projectSnapshot: CurationResult['project_snapshot'] = undefined
308
+ const sessionSummaries: string[] = []
309
+ const interactionTones: string[] = []
310
+ const projectSnapshots: NonNullable<CurationResult['project_snapshot']>[] = []
311
311
 
312
312
  const spinner = new Spinner()
313
313
 
@@ -332,15 +332,15 @@ export async function ingest(options: IngestOptions) {
332
332
  totalMemories++
333
333
  }
334
334
 
335
- // Keep the most recent session summary, tone, and snapshot
335
+ // Accumulate ALL session summaries, tones, and snapshots (not just latest)
336
336
  if (result.session_summary) {
337
- sessionSummary = result.session_summary
337
+ sessionSummaries.push(result.session_summary)
338
338
  }
339
339
  if (result.interaction_tone) {
340
- interactionTone = result.interaction_tone
340
+ interactionTones.push(result.interaction_tone)
341
341
  }
342
342
  if (result.project_snapshot) {
343
- projectSnapshot = result.project_snapshot
343
+ projectSnapshots.push(result.project_snapshot)
344
344
  }
345
345
  } catch (error: any) {
346
346
  failedSegments++
@@ -348,17 +348,59 @@ export async function ingest(options: IngestOptions) {
348
348
  }
349
349
  }
350
350
 
351
+ // Combine summaries from all segments (chronological order)
352
+ let combinedSummary = ''
353
+ if (sessionSummaries.length === 1) {
354
+ combinedSummary = sessionSummaries[0]!
355
+ } else if (sessionSummaries.length > 1) {
356
+ combinedSummary = sessionSummaries
357
+ .map((s, i) => `[Part ${i + 1}/${sessionSummaries.length}] ${s}`)
358
+ .join('\n\n')
359
+ }
360
+
361
+ // For interaction tone, use the last one (most recent)
362
+ const finalTone = interactionTones.length > 0
363
+ ? interactionTones[interactionTones.length - 1]!
364
+ : ''
365
+
366
+ // For project snapshot, merge all - later ones take precedence for phase
367
+ let mergedSnapshot: CurationResult['project_snapshot'] | undefined
368
+ if (projectSnapshots.length > 0) {
369
+ const allAchievements: string[] = []
370
+ const allChallenges: string[] = []
371
+ const allNextSteps: string[] = []
372
+
373
+ for (const snap of projectSnapshots) {
374
+ if (snap.recent_achievements) allAchievements.push(...snap.recent_achievements)
375
+ if (snap.active_challenges) allChallenges.push(...snap.active_challenges)
376
+ if (snap.next_steps) allNextSteps.push(...snap.next_steps)
377
+ }
378
+
379
+ const lastSnapshot = projectSnapshots[projectSnapshots.length - 1]!
380
+ mergedSnapshot = {
381
+ id: lastSnapshot.id || '',
382
+ session_id: lastSnapshot.session_id || '',
383
+ project_id: lastSnapshot.project_id || '',
384
+ current_phase: lastSnapshot.current_phase,
385
+ recent_achievements: [...new Set(allAchievements)],
386
+ active_challenges: [...new Set(allChallenges)],
387
+ next_steps: [...new Set(allNextSteps)],
388
+ created_at: lastSnapshot.created_at || Date.now(),
389
+ }
390
+ }
391
+
351
392
  // Store session summary and project snapshot
352
- if (sessionSummary) {
353
- await store.storeSessionSummary(project.folderId, session.id, sessionSummary, interactionTone)
393
+ if (combinedSummary) {
394
+ await store.storeSessionSummary(project.folderId, session.id, combinedSummary, finalTone)
354
395
  if (options.verbose) {
355
- console.log(` ${style('dim', `Summary stored: ${sessionSummary.slice(0, 60)}...`)}`)
396
+ const preview = combinedSummary.length > 60 ? combinedSummary.slice(0, 57) + '...' : combinedSummary
397
+ console.log(` ${style('dim', `Summary stored (${sessionSummaries.length} parts): ${preview}`)}`)
356
398
  }
357
399
  }
358
- if (projectSnapshot) {
359
- await store.storeProjectSnapshot(project.folderId, session.id, projectSnapshot)
400
+ if (mergedSnapshot) {
401
+ await store.storeProjectSnapshot(project.folderId, session.id, mergedSnapshot)
360
402
  if (options.verbose) {
361
- console.log(` ${style('dim', `Snapshot stored: phase=${projectSnapshot.current_phase || 'none'}`)}`)
403
+ console.log(` ${style('dim', `Snapshot stored: phase=${mergedSnapshot.current_phase || 'none'}, ${mergedSnapshot.recent_achievements?.length || 0} achievements`)}`)
362
404
  }
363
405
  }
364
406
 
@@ -368,12 +410,12 @@ export async function ingest(options: IngestOptions) {
368
410
  // Start spinner for manager
369
411
  spinner.start(`Managing ${sessionMemories.length} memories - organizing with Opus 4.5...`)
370
412
 
371
- // Build curation result for manager
413
+ // Build curation result for manager (using combined/merged values)
372
414
  const curationResult: CurationResult = {
373
415
  memories: sessionMemories,
374
- session_summary: sessionSummary,
375
- interaction_tone: interactionTone,
376
- project_snapshot: projectSnapshot,
416
+ session_summary: combinedSummary,
417
+ interaction_tone: finalTone,
418
+ project_snapshot: mergedSnapshot,
377
419
  }
378
420
 
379
421
  const managerResult = await manager.manageWithSDK(
@@ -861,8 +861,9 @@ This session has ended. Please curate the memories from this conversation accord
861
861
  }
862
862
 
863
863
  /**
864
- * Find and curate from a session file directly
864
+ * Find and curate from a session file directly (LEGACY - no segmentation)
865
865
  * Uses SDK mode to avoid CLI output truncation issues
866
+ * @deprecated Use curateFromSessionFileWithSegments() for large sessions
866
867
  */
867
868
  async curateFromSessionFile(
868
869
  sessionId: string,
@@ -897,6 +898,200 @@ This session has ended. Please curate the memories from this conversation accord
897
898
  return this.curateWithSDK(session.messages as any, triggerType);
898
899
  }
899
900
 
901
+ /**
902
+ * Find and curate from a session file with SEGMENTATION
903
+ * Breaks large sessions into segments and curates each one
904
+ * This matches the behavior of the ingest command
905
+ *
906
+ * @param sessionId - The session ID to curate
907
+ * @param triggerType - What triggered curation
908
+ * @param cwd - Working directory hint for finding the session
909
+ * @param maxTokensPerSegment - Max tokens per segment (default: 150000)
910
+ * @param onSegmentProgress - Optional callback for progress updates
911
+ */
912
+ async curateFromSessionFileWithSegments(
913
+ sessionId: string,
914
+ triggerType: CurationTrigger = "session_end",
915
+ cwd?: string,
916
+ maxTokensPerSegment = 150000,
917
+ onSegmentProgress?: (progress: {
918
+ segmentIndex: number;
919
+ totalSegments: number;
920
+ memoriesExtracted: number;
921
+ tokensInSegment: number;
922
+ }) => void,
923
+ ): Promise<CurationResult> {
924
+ // Find the session file
925
+ const sessionFile = await this._findSessionFile(sessionId, cwd);
926
+ if (!sessionFile) {
927
+ logger.debug(
928
+ `Curator: Could not find session file for ${sessionId}`,
929
+ "curator",
930
+ );
931
+ return { session_summary: "", memories: [] };
932
+ }
933
+
934
+ logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
935
+
936
+ // Parse the session to get metadata first
937
+ const session = await parseSessionFile(sessionFile);
938
+ if (session.messages.length === 0) {
939
+ logger.debug("Curator: Session has no messages", "curator");
940
+ return { session_summary: "", memories: [] };
941
+ }
942
+
943
+ // Log detailed session stats
944
+ const { metadata } = session;
945
+ logger.debug(
946
+ `Curator: Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
947
+ "curator",
948
+ );
949
+ logger.debug(
950
+ `Curator: Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
951
+ "curator",
952
+ );
953
+
954
+ // Parse into segments using the same function as ingest
955
+ const { parseSessionFileWithSegments } = await import("./session-parser.ts");
956
+ const segments = await parseSessionFileWithSegments(sessionFile, maxTokensPerSegment);
957
+
958
+ if (segments.length === 0) {
959
+ logger.debug("Curator: No segments found in session", "curator");
960
+ return { session_summary: "", memories: [] };
961
+ }
962
+
963
+ logger.debug(
964
+ `Curator: Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
965
+ "curator",
966
+ );
967
+
968
+ // Accumulate results from all segments
969
+ const allMemories: CuratedMemory[] = [];
970
+ const sessionSummaries: string[] = [];
971
+ const interactionTones: string[] = [];
972
+ const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] = [];
973
+ let failedSegments = 0;
974
+
975
+ // Curate each segment
976
+ for (const segment of segments) {
977
+ const segmentLabel = `${segment.segmentIndex + 1}/${segment.totalSegments}`;
978
+ const tokensLabel = `${Math.round(segment.estimatedTokens / 1000)}k`;
979
+
980
+ logger.debug(
981
+ `Curator: Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
982
+ "curator",
983
+ );
984
+
985
+ try {
986
+ // Curate this segment
987
+ const result = await this.curateFromSegment(segment, triggerType);
988
+
989
+ // Accumulate memories
990
+ allMemories.push(...result.memories);
991
+
992
+ // Accumulate ALL session summaries, tones, and snapshots (not just latest)
993
+ if (result.session_summary) {
994
+ sessionSummaries.push(result.session_summary);
995
+ }
996
+ if (result.interaction_tone) {
997
+ interactionTones.push(result.interaction_tone);
998
+ }
999
+ if (result.project_snapshot) {
1000
+ projectSnapshots.push(result.project_snapshot);
1001
+ }
1002
+
1003
+ logger.debug(
1004
+ `Curator: Segment ${segmentLabel} extracted ${result.memories.length} memories`,
1005
+ "curator",
1006
+ );
1007
+
1008
+ // Progress callback
1009
+ if (onSegmentProgress) {
1010
+ onSegmentProgress({
1011
+ segmentIndex: segment.segmentIndex,
1012
+ totalSegments: segment.totalSegments,
1013
+ memoriesExtracted: result.memories.length,
1014
+ tokensInSegment: segment.estimatedTokens,
1015
+ });
1016
+ }
1017
+ } catch (error: any) {
1018
+ failedSegments++;
1019
+ logger.debug(
1020
+ `Curator: Segment ${segmentLabel} failed: ${error.message}`,
1021
+ "curator",
1022
+ );
1023
+ }
1024
+ }
1025
+
1026
+ // Log final summary
1027
+ if (failedSegments > 0) {
1028
+ logger.debug(
1029
+ `Curator: Completed with ${failedSegments} failed segment(s)`,
1030
+ "curator",
1031
+ );
1032
+ }
1033
+ logger.debug(
1034
+ `Curator: Total ${allMemories.length} memories from ${segments.length} segment(s)`,
1035
+ "curator",
1036
+ );
1037
+ logger.debug(
1038
+ `Curator: Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
1039
+ "curator",
1040
+ );
1041
+
1042
+ // Combine summaries from all segments (chronological order)
1043
+ // For single segment, just use the summary directly
1044
+ // For multiple segments, join them with segment markers
1045
+ let combinedSummary = "";
1046
+ if (sessionSummaries.length === 1) {
1047
+ combinedSummary = sessionSummaries[0]!;
1048
+ } else if (sessionSummaries.length > 1) {
1049
+ combinedSummary = sessionSummaries
1050
+ .map((s, i) => `[Part ${i + 1}/${sessionSummaries.length}] ${s}`)
1051
+ .join("\n\n");
1052
+ }
1053
+
1054
+ // For interaction tone, use the most common one or the last one
1055
+ const finalTone = interactionTones.length > 0
1056
+ ? interactionTones[interactionTones.length - 1]
1057
+ : undefined;
1058
+
1059
+ // For project snapshot, merge all snapshots - later ones take precedence for phase,
1060
+ // but accumulate achievements/challenges/next_steps
1061
+ let mergedSnapshot: CurationResult["project_snapshot"] | undefined;
1062
+ if (projectSnapshots.length > 0) {
1063
+ const allAchievements: string[] = [];
1064
+ const allChallenges: string[] = [];
1065
+ const allNextSteps: string[] = [];
1066
+
1067
+ for (const snap of projectSnapshots) {
1068
+ if (snap.recent_achievements) allAchievements.push(...snap.recent_achievements);
1069
+ if (snap.active_challenges) allChallenges.push(...snap.active_challenges);
1070
+ if (snap.next_steps) allNextSteps.push(...snap.next_steps);
1071
+ }
1072
+
1073
+ // Use the LAST snapshot's phase (most recent state)
1074
+ const lastSnapshot = projectSnapshots[projectSnapshots.length - 1]!;
1075
+ mergedSnapshot = {
1076
+ id: lastSnapshot.id || "",
1077
+ session_id: lastSnapshot.session_id || "",
1078
+ project_id: lastSnapshot.project_id || "",
1079
+ current_phase: lastSnapshot.current_phase,
1080
+ recent_achievements: [...new Set(allAchievements)], // dedupe
1081
+ active_challenges: [...new Set(allChallenges)],
1082
+ next_steps: [...new Set(allNextSteps)],
1083
+ created_at: lastSnapshot.created_at || Date.now(),
1084
+ };
1085
+ }
1086
+
1087
+ return {
1088
+ session_summary: combinedSummary,
1089
+ interaction_tone: finalTone,
1090
+ project_snapshot: mergedSnapshot,
1091
+ memories: allMemories,
1092
+ };
1093
+ }
1094
+
900
1095
  /**
901
1096
  * Find the session file path given a session ID
902
1097
  */
@@ -1168,6 +1363,7 @@ This session has ended. Please curate the memories from this conversation accord
1168
1363
 
1169
1364
  /**
1170
1365
  * Fallback to SDK mode when CLI mode fails (e.g., output truncation)
1366
+ * Now uses segmented approach for large sessions
1171
1367
  */
1172
1368
  private async _fallbackToSDK(
1173
1369
  sessionId: string,
@@ -1175,10 +1371,18 @@ This session has ended. Please curate the memories from this conversation accord
1175
1371
  cwd?: string,
1176
1372
  ): Promise<CurationResult> {
1177
1373
  try {
1178
- const result = await this.curateFromSessionFile(
1374
+ // Use segmented approach - same as ingest command
1375
+ const result = await this.curateFromSessionFileWithSegments(
1179
1376
  sessionId,
1180
1377
  triggerType,
1181
1378
  cwd,
1379
+ 150000, // 150k tokens per segment
1380
+ (progress) => {
1381
+ logger.debug(
1382
+ `Curator fallback: Segment ${progress.segmentIndex + 1}/${progress.totalSegments} → ${progress.memoriesExtracted} memories`,
1383
+ "curator",
1384
+ );
1385
+ },
1182
1386
  );
1183
1387
  if (result.memories.length > 0) {
1184
1388
  logger.debug(
@@ -198,19 +198,27 @@ export async function createServer(config: ServerConfig = {}) {
198
198
  setImmediate(async () => {
199
199
  try {
200
200
  // Try session resume first (v2) - gets full context including tool uses
201
- // Falls back to transcript parsing if resume fails
201
+ // Falls back to segmented transcript parsing if resume fails
202
202
  let result = await curator.curateWithSessionResume(
203
203
  body.claude_session_id,
204
204
  body.trigger
205
205
  )
206
206
 
207
- // Fallback to transcript-based curation if resume returned nothing
207
+ // Fallback to transcript-based curation WITH SEGMENTATION if resume returned nothing
208
+ // This matches the ingest command behavior - breaks large sessions into segments
208
209
  if (result.memories.length === 0) {
209
- logger.debug('Session resume returned no memories, falling back to transcript parsing', 'server')
210
- result = await curator.curateFromSessionFile(
210
+ logger.debug('Session resume returned no memories, falling back to segmented transcript parsing', 'server')
211
+ result = await curator.curateFromSessionFileWithSegments(
211
212
  body.claude_session_id,
212
213
  body.trigger,
213
- body.cwd
214
+ body.cwd,
215
+ 150000, // 150k tokens per segment
216
+ (progress) => {
217
+ logger.debug(
218
+ `Curation segment ${progress.segmentIndex + 1}/${progress.totalSegments}: ${progress.memoriesExtracted} memories (~${Math.round(progress.tokensInSegment / 1000)}k tokens)`,
219
+ 'server'
220
+ )
221
+ }
214
222
  )
215
223
  }
216
224