@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 +1 -1
- package/src/cli/commands/ingest.ts +60 -18
- package/src/core/curator.ts +206 -2
- package/src/server/index.ts +13 -5
package/package.json
CHANGED
|
@@ -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
|
|
306
|
+
// Accumulate all results from this session for manager
|
|
307
307
|
const sessionMemories: CuratedMemory[] = []
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
335
|
+
// Accumulate ALL session summaries, tones, and snapshots (not just latest)
|
|
336
336
|
if (result.session_summary) {
|
|
337
|
-
|
|
337
|
+
sessionSummaries.push(result.session_summary)
|
|
338
338
|
}
|
|
339
339
|
if (result.interaction_tone) {
|
|
340
|
-
|
|
340
|
+
interactionTones.push(result.interaction_tone)
|
|
341
341
|
}
|
|
342
342
|
if (result.project_snapshot) {
|
|
343
|
-
|
|
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 (
|
|
353
|
-
await store.storeSessionSummary(project.folderId, session.id,
|
|
393
|
+
if (combinedSummary) {
|
|
394
|
+
await store.storeSessionSummary(project.folderId, session.id, combinedSummary, finalTone)
|
|
354
395
|
if (options.verbose) {
|
|
355
|
-
|
|
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 (
|
|
359
|
-
await store.storeProjectSnapshot(project.folderId, session.id,
|
|
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=${
|
|
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:
|
|
375
|
-
interaction_tone:
|
|
376
|
-
project_snapshot:
|
|
416
|
+
session_summary: combinedSummary,
|
|
417
|
+
interaction_tone: finalTone,
|
|
418
|
+
project_snapshot: mergedSnapshot,
|
|
377
419
|
}
|
|
378
420
|
|
|
379
421
|
const managerResult = await manager.manageWithSDK(
|
package/src/core/curator.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
package/src/server/index.ts
CHANGED
|
@@ -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.
|
|
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
|
|