@rlabs-inc/memory 0.4.12 → 0.4.14
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/README.md +25 -0
- package/package.json +1 -1
- package/src/cli/commands/ingest.ts +60 -18
- package/src/core/curator.ts +206 -2
- package/src/core/engine.ts +76 -2
- package/src/core/retrieval.ts +62 -0
- package/src/server/index.ts +40 -14
package/README.md
CHANGED
|
@@ -46,6 +46,21 @@ That's it. Now use Claude Code normally—memories are extracted and surfaced au
|
|
|
46
46
|
|
|
47
47
|
## Features
|
|
48
48
|
|
|
49
|
+
### Action Items Signal (`***`)
|
|
50
|
+
Add `***` at the end of any message to retrieve all pending action items:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
"hey Watson, let's continue with the project ***"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This returns all memories marked as:
|
|
57
|
+
- `action_required: true`
|
|
58
|
+
- `awaiting_implementation: true`
|
|
59
|
+
- `awaiting_decision: true`
|
|
60
|
+
- `context_type: 'unresolved'`
|
|
61
|
+
|
|
62
|
+
Zero overhead for normal messages—detection is a simple `endsWith` check.
|
|
63
|
+
|
|
49
64
|
### Semantic Embeddings
|
|
50
65
|
Uses `all-MiniLM-L6-v2` for 384-dimensional embeddings. Memories are retrieved by meaning, not just keywords.
|
|
51
66
|
|
|
@@ -383,6 +398,16 @@ This isn't just about remembering facts. It's about preserving:
|
|
|
383
398
|
|
|
384
399
|
## Changelog
|
|
385
400
|
|
|
401
|
+
### v0.4.14
|
|
402
|
+
- **Feature**: Action items signal (`***`) - add to end of message to retrieve all pending items
|
|
403
|
+
- **Feature**: New `getActionItems()` retrieval function with special formatting
|
|
404
|
+
- Zero overhead for normal messages - detection is a simple string check
|
|
405
|
+
|
|
406
|
+
### v0.4.13
|
|
407
|
+
- **Fix**: Segmented transcript curation for large sessions (400+ messages)
|
|
408
|
+
- **Improvement**: Unified curator fallback and ingest command behavior
|
|
409
|
+
- **Tech**: Segments at ~150k tokens, accumulates memories, merges summaries with part markers
|
|
410
|
+
|
|
386
411
|
### v0.4.12
|
|
387
412
|
- **Simplify**: Removed Zod structured outputs - session resumption only
|
|
388
413
|
- **Improvement**: Uses existing battle-tested JSON parser instead of Zod
|
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/core/engine.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { homedir } from 'os'
|
|
7
7
|
import { join } from 'path'
|
|
8
8
|
import { MemoryStore, createStore } from './store.ts'
|
|
9
|
-
import { SmartVectorRetrieval, createRetrieval, type SessionContext } from './retrieval.ts'
|
|
9
|
+
import { SmartVectorRetrieval, createRetrieval, getActionItems, type SessionContext } from './retrieval.ts'
|
|
10
10
|
import type {
|
|
11
11
|
CuratedMemory,
|
|
12
12
|
StoredMemory,
|
|
@@ -66,6 +66,13 @@ export interface EngineConfig {
|
|
|
66
66
|
personalMemoriesEnabled?: boolean
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Retrieval mode
|
|
71
|
+
* - 'normal': Standard activation signal retrieval (default)
|
|
72
|
+
* - 'action_items': Return all memories marked as requiring action
|
|
73
|
+
*/
|
|
74
|
+
export type RetrievalMode = 'normal' | 'action_items'
|
|
75
|
+
|
|
69
76
|
/**
|
|
70
77
|
* Context request parameters
|
|
71
78
|
*/
|
|
@@ -75,6 +82,7 @@ export interface ContextRequest {
|
|
|
75
82
|
currentMessage: string
|
|
76
83
|
maxMemories?: number
|
|
77
84
|
projectPath?: string // Required for 'local' storage mode
|
|
85
|
+
mode?: RetrievalMode // Retrieval mode (default: 'normal')
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
@@ -212,7 +220,23 @@ export class MemoryEngine {
|
|
|
212
220
|
return { memories: [], formatted: '' }
|
|
213
221
|
}
|
|
214
222
|
|
|
215
|
-
//
|
|
223
|
+
// ACTION ITEMS MODE: Return all memories marked as requiring action
|
|
224
|
+
// Triggered by *** signal at end of message
|
|
225
|
+
if (request.mode === 'action_items') {
|
|
226
|
+
const actionItems = getActionItems(allMemories, projectId)
|
|
227
|
+
|
|
228
|
+
// Update injected memories for deduplication
|
|
229
|
+
for (const memory of actionItems) {
|
|
230
|
+
injectedIds.add(memory.id)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
memories: actionItems,
|
|
235
|
+
formatted: this._formatActionItems(actionItems),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// NORMAL MODE: Filter out already-injected memories (deduplication)
|
|
216
240
|
const candidateMemories = allMemories.filter(m => !injectedIds.has(m.id))
|
|
217
241
|
|
|
218
242
|
if (!candidateMemories.length) {
|
|
@@ -628,6 +652,56 @@ export class MemoryEngine {
|
|
|
628
652
|
return parts.join('\n')
|
|
629
653
|
}
|
|
630
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Format action items for injection
|
|
657
|
+
* Different header to make it clear this is the full action items list
|
|
658
|
+
*/
|
|
659
|
+
private _formatActionItems(memories: RetrievalResult[]): string {
|
|
660
|
+
if (!memories.length) {
|
|
661
|
+
return '# Action Items\n\nNo pending action items found.'
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const parts: string[] = ['# Action Items']
|
|
665
|
+
parts.push(`\n*${memories.length} pending item${memories.length === 1 ? '' : 's'}*\n`)
|
|
666
|
+
|
|
667
|
+
for (const memory of memories) {
|
|
668
|
+
const importance = memory.importance_weight?.toFixed(1) || '0.5'
|
|
669
|
+
const emoji = getMemoryEmoji(memory.context_type || 'general')
|
|
670
|
+
const age = memory.updated_at ? this._formatAge(memory.updated_at) :
|
|
671
|
+
memory.created_at ? this._formatAge(memory.created_at) : ''
|
|
672
|
+
|
|
673
|
+
// Flags
|
|
674
|
+
const flags: string[] = []
|
|
675
|
+
if (memory.action_required) flags.push('⚡ACTION')
|
|
676
|
+
if (memory.awaiting_implementation) flags.push('🔨IMPL')
|
|
677
|
+
if (memory.awaiting_decision) flags.push('❓DECISION')
|
|
678
|
+
if (memory.context_type === 'unresolved') flags.push('❓UNRESOLVED')
|
|
679
|
+
const flagStr = flags.length ? ` [${flags.join(' ')}]` : ''
|
|
680
|
+
|
|
681
|
+
// Short ID for reference
|
|
682
|
+
const shortId = memory.id.slice(-6)
|
|
683
|
+
|
|
684
|
+
// Display: headline if available, otherwise content
|
|
685
|
+
const displayText = memory.headline || memory.content
|
|
686
|
+
|
|
687
|
+
parts.push(`[${emoji} ${importance} • ${age} • #${shortId}]${flagStr}`)
|
|
688
|
+
parts.push(`${displayText}`)
|
|
689
|
+
|
|
690
|
+
// Always show full content for action items (they need context)
|
|
691
|
+
if (memory.headline && memory.content) {
|
|
692
|
+
const contentLines = memory.content.split('\n')
|
|
693
|
+
for (const line of contentLines) {
|
|
694
|
+
if (line.trim()) {
|
|
695
|
+
parts.push(` ${line}`)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
parts.push('') // Blank line between items
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return parts.join('\n')
|
|
703
|
+
}
|
|
704
|
+
|
|
631
705
|
/**
|
|
632
706
|
* Get resolved storage paths for a project
|
|
633
707
|
* Returns the actual paths based on current engine configuration
|
package/src/core/retrieval.ts
CHANGED
|
@@ -777,3 +777,65 @@ export class SmartVectorRetrieval {
|
|
|
777
777
|
export function createRetrieval(): SmartVectorRetrieval {
|
|
778
778
|
return new SmartVectorRetrieval()
|
|
779
779
|
}
|
|
780
|
+
|
|
781
|
+
// ============================================================================
|
|
782
|
+
// ACTION ITEMS RETRIEVAL
|
|
783
|
+
// Returns all memories marked as requiring action
|
|
784
|
+
// Triggered by *** signal at end of message (detected in hook)
|
|
785
|
+
// ============================================================================
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Get all memories marked as requiring action
|
|
789
|
+
*
|
|
790
|
+
* Filters for memories with:
|
|
791
|
+
* - action_required: true
|
|
792
|
+
* - awaiting_implementation: true
|
|
793
|
+
* - awaiting_decision: true
|
|
794
|
+
* - context_type: 'todo'
|
|
795
|
+
*
|
|
796
|
+
* Returns them sorted by importance, newest first within same importance
|
|
797
|
+
*/
|
|
798
|
+
export function getActionItems(
|
|
799
|
+
allMemories: StoredMemory[],
|
|
800
|
+
currentProjectId: string
|
|
801
|
+
): RetrievalResult[] {
|
|
802
|
+
// Filter to active action items
|
|
803
|
+
const actionItems = allMemories.filter(memory => {
|
|
804
|
+
// Must be active
|
|
805
|
+
if (memory.status && memory.status !== 'active') return false
|
|
806
|
+
if (memory.exclude_from_retrieval === true) return false
|
|
807
|
+
|
|
808
|
+
// Must be for this project or global
|
|
809
|
+
const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
|
|
810
|
+
if (!isGlobal && memory.project_id !== currentProjectId) return false
|
|
811
|
+
|
|
812
|
+
// Must have at least one action flag
|
|
813
|
+
return (
|
|
814
|
+
memory.action_required === true ||
|
|
815
|
+
memory.awaiting_implementation === true ||
|
|
816
|
+
memory.awaiting_decision === true ||
|
|
817
|
+
memory.context_type === 'unresolved' // unresolved = todos, blockers, open questions
|
|
818
|
+
)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
// Sort by importance (desc), then by created_at (desc = newest first)
|
|
822
|
+
actionItems.sort((a, b) => {
|
|
823
|
+
const aImportance = a.importance_weight ?? 0.5
|
|
824
|
+
const bImportance = b.importance_weight ?? 0.5
|
|
825
|
+
if (bImportance !== aImportance) {
|
|
826
|
+
return bImportance - aImportance
|
|
827
|
+
}
|
|
828
|
+
// Newest first
|
|
829
|
+
const aTime = a.created_at ?? 0
|
|
830
|
+
const bTime = b.created_at ?? 0
|
|
831
|
+
return bTime - aTime
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
// Convert to result format
|
|
835
|
+
return actionItems.map(memory => ({
|
|
836
|
+
...memory,
|
|
837
|
+
score: 1.0, // All action items are relevant by definition
|
|
838
|
+
relevance_score: 1.0,
|
|
839
|
+
value_score: memory.importance_weight ?? 0.5,
|
|
840
|
+
}))
|
|
841
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface ContextRequest {
|
|
|
43
43
|
current_message?: string
|
|
44
44
|
max_memories?: number
|
|
45
45
|
project_path?: string
|
|
46
|
+
mode?: 'normal' | 'action_items' // Can be set explicitly or auto-detected via ***
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
interface ProcessRequest {
|
|
@@ -136,26 +137,42 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
136
137
|
|
|
137
138
|
logger.request('POST', '/memory/context', body.project_id)
|
|
138
139
|
|
|
140
|
+
// Detect *** signal at end of message for action items mode
|
|
141
|
+
let message = body.current_message ?? ''
|
|
142
|
+
let mode = body.mode ?? 'normal'
|
|
143
|
+
|
|
144
|
+
if (message.trimEnd().endsWith('***')) {
|
|
145
|
+
mode = 'action_items'
|
|
146
|
+
// Strip the *** signal from the message
|
|
147
|
+
message = message.trimEnd().slice(0, -3).trimEnd()
|
|
148
|
+
logger.debug('Action items mode detected (*** signal)', 'server')
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
const result = await engine.getContext({
|
|
140
152
|
sessionId: body.session_id,
|
|
141
153
|
projectId: body.project_id,
|
|
142
|
-
currentMessage:
|
|
154
|
+
currentMessage: message,
|
|
143
155
|
maxMemories: body.max_memories,
|
|
144
156
|
projectPath: body.project_path,
|
|
157
|
+
mode,
|
|
145
158
|
})
|
|
146
159
|
|
|
147
160
|
// Log what happened
|
|
148
161
|
if (result.primer) {
|
|
149
162
|
logger.primer(`Session primer for ${body.project_id}`)
|
|
150
163
|
} else if (result.memories.length > 0) {
|
|
151
|
-
|
|
152
|
-
result.memories.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
if (mode === 'action_items') {
|
|
165
|
+
logger.info(`Returning ${result.memories.length} action item${result.memories.length === 1 ? '' : 's'}`)
|
|
166
|
+
} else {
|
|
167
|
+
logger.logRetrievedMemories(
|
|
168
|
+
result.memories.map(m => ({
|
|
169
|
+
content: m.content,
|
|
170
|
+
score: m.score,
|
|
171
|
+
context_type: m.context_type,
|
|
172
|
+
})),
|
|
173
|
+
message
|
|
174
|
+
)
|
|
175
|
+
}
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
return Response.json({
|
|
@@ -167,6 +184,7 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
167
184
|
curator_enabled: true,
|
|
168
185
|
memories_count: result.memories.length,
|
|
169
186
|
has_primer: !!result.primer,
|
|
187
|
+
mode, // Include the mode that was used
|
|
170
188
|
}, { headers: corsHeaders })
|
|
171
189
|
}
|
|
172
190
|
|
|
@@ -198,19 +216,27 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
198
216
|
setImmediate(async () => {
|
|
199
217
|
try {
|
|
200
218
|
// Try session resume first (v2) - gets full context including tool uses
|
|
201
|
-
// Falls back to transcript parsing if resume fails
|
|
219
|
+
// Falls back to segmented transcript parsing if resume fails
|
|
202
220
|
let result = await curator.curateWithSessionResume(
|
|
203
221
|
body.claude_session_id,
|
|
204
222
|
body.trigger
|
|
205
223
|
)
|
|
206
224
|
|
|
207
|
-
// Fallback to transcript-based curation if resume returned nothing
|
|
225
|
+
// Fallback to transcript-based curation WITH SEGMENTATION if resume returned nothing
|
|
226
|
+
// This matches the ingest command behavior - breaks large sessions into segments
|
|
208
227
|
if (result.memories.length === 0) {
|
|
209
|
-
logger.debug('Session resume returned no memories, falling back to transcript parsing', 'server')
|
|
210
|
-
result = await curator.
|
|
228
|
+
logger.debug('Session resume returned no memories, falling back to segmented transcript parsing', 'server')
|
|
229
|
+
result = await curator.curateFromSessionFileWithSegments(
|
|
211
230
|
body.claude_session_id,
|
|
212
231
|
body.trigger,
|
|
213
|
-
body.cwd
|
|
232
|
+
body.cwd,
|
|
233
|
+
150000, // 150k tokens per segment
|
|
234
|
+
(progress) => {
|
|
235
|
+
logger.debug(
|
|
236
|
+
`Curation segment ${progress.segmentIndex + 1}/${progress.totalSegments}: ${progress.memoriesExtracted} memories (~${Math.round(progress.tokensInSegment / 1000)}k tokens)`,
|
|
237
|
+
'server'
|
|
238
|
+
)
|
|
239
|
+
}
|
|
214
240
|
)
|
|
215
241
|
}
|
|
216
242
|
|