@rlabs-inc/memory 0.4.11 → 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/README.md +9 -4
- package/dist/index.js +3296 -16926
- package/dist/index.mjs +3290 -16920
- package/dist/server/index.js +3844 -17474
- package/dist/server/index.mjs +3838 -17468
- package/package.json +3 -4
- package/src/cli/commands/ingest.ts +60 -18
- package/src/core/curator.ts +236 -86
- package/src/server/index.ts +13 -5
- package/src/types/curation-schema.ts +0 -107
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlabs-inc/memory",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
|
@@ -39,11 +39,10 @@
|
|
|
39
39
|
"cli": "bun src/cli/index.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
42
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.12",
|
|
43
43
|
"@huggingface/transformers": "^3.4.1",
|
|
44
44
|
"@rlabs-inc/fsdb": "^1.0.1",
|
|
45
|
-
"@rlabs-inc/signals": "^1.0.0"
|
|
46
|
-
"zod": "^4.3.5"
|
|
45
|
+
"@rlabs-inc/signals": "^1.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"typescript": "^5.0.0",
|
|
@@ -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
|
@@ -14,8 +14,6 @@ import type {
|
|
|
14
14
|
ContextType,
|
|
15
15
|
} from "../types/memory.ts";
|
|
16
16
|
import { logger } from "../utils/logger.ts";
|
|
17
|
-
import { CurationResultSchema, type ZodCurationResult } from "../types/curation-schema.ts";
|
|
18
|
-
import { z } from "zod";
|
|
19
17
|
import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
|
|
20
18
|
|
|
21
19
|
/**
|
|
@@ -712,13 +710,12 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
712
710
|
}
|
|
713
711
|
|
|
714
712
|
/**
|
|
715
|
-
* Curate using session resumption
|
|
713
|
+
* Curate using session resumption (v2)
|
|
716
714
|
*
|
|
717
715
|
* This is the preferred method when we have a Claude session ID.
|
|
718
716
|
* Benefits over transcript parsing:
|
|
719
717
|
* - Claude sees FULL context including tool uses, results, thinking
|
|
720
|
-
* -
|
|
721
|
-
* - SDK auto-retries if output doesn't match schema
|
|
718
|
+
* - No transcript parsing errors or truncation
|
|
722
719
|
*
|
|
723
720
|
* @param claudeSessionId - The actual Claude Code session ID (resumable)
|
|
724
721
|
* @param triggerType - What triggered curation (session_end, pre_compact, etc.)
|
|
@@ -731,28 +728,25 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
731
728
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
732
729
|
|
|
733
730
|
const curationPrompt = this.buildCurationPrompt(triggerType);
|
|
734
|
-
const jsonSchema = z.toJSONSchema(CurationResultSchema);
|
|
735
731
|
|
|
736
732
|
logger.debug(
|
|
737
|
-
`Curator v2: Resuming session ${claudeSessionId}
|
|
733
|
+
`Curator v2: Resuming session ${claudeSessionId}`,
|
|
738
734
|
"curator",
|
|
739
735
|
);
|
|
740
736
|
|
|
741
737
|
try {
|
|
742
738
|
const q = query({
|
|
743
|
-
prompt: "Curate memories from this session according to your system instructions. Return the
|
|
739
|
+
prompt: "Curate memories from this session according to your system instructions. Return ONLY the JSON structure.",
|
|
744
740
|
options: {
|
|
745
741
|
resume: claudeSessionId,
|
|
746
742
|
appendSystemPrompt: curationPrompt, // APPEND, don't replace!
|
|
747
743
|
model: "claude-opus-4-5-20251101",
|
|
748
744
|
permissionMode: "bypassPermissions",
|
|
749
|
-
outputFormat: {
|
|
750
|
-
type: "json_schema",
|
|
751
|
-
schema: jsonSchema,
|
|
752
|
-
},
|
|
753
745
|
},
|
|
754
746
|
});
|
|
755
747
|
|
|
748
|
+
// Collect the result
|
|
749
|
+
let resultText = "";
|
|
756
750
|
for await (const message of q) {
|
|
757
751
|
// Track usage for debugging
|
|
758
752
|
if (message.type === "assistant" && "usage" in message && message.usage) {
|
|
@@ -762,45 +756,40 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
762
756
|
);
|
|
763
757
|
}
|
|
764
758
|
|
|
765
|
-
//
|
|
759
|
+
// Get the result text
|
|
766
760
|
if (message.type === "result") {
|
|
767
|
-
if (message.subtype === "
|
|
768
|
-
// Validate with Zod (belt + suspenders)
|
|
769
|
-
const parsed = CurationResultSchema.safeParse(message.structured_output);
|
|
770
|
-
|
|
771
|
-
if (parsed.success) {
|
|
772
|
-
logger.debug(
|
|
773
|
-
`Curator v2: Extracted ${parsed.data.memories.length} memories via structured output`,
|
|
774
|
-
"curator",
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
// Convert ZodCurationResult to CurationResult (add missing fields)
|
|
778
|
-
return this._zodResultToCurationResult(parsed.data);
|
|
779
|
-
} else {
|
|
780
|
-
logger.debug(
|
|
781
|
-
`Curator v2: Zod validation failed: ${parsed.error.message}`,
|
|
782
|
-
"curator",
|
|
783
|
-
);
|
|
784
|
-
return { session_summary: "", memories: [] };
|
|
785
|
-
}
|
|
786
|
-
} else if (message.subtype === "error_max_structured_output_retries") {
|
|
787
|
-
logger.debug(
|
|
788
|
-
"Curator v2: SDK failed to produce valid output after retries",
|
|
789
|
-
"curator",
|
|
790
|
-
);
|
|
791
|
-
return { session_summary: "", memories: [] };
|
|
792
|
-
} else if (message.subtype === "error") {
|
|
761
|
+
if (message.subtype === "error") {
|
|
793
762
|
logger.debug(
|
|
794
763
|
`Curator v2: Error result - ${JSON.stringify(message)}`,
|
|
795
764
|
"curator",
|
|
796
765
|
);
|
|
797
766
|
return { session_summary: "", memories: [] };
|
|
767
|
+
} else if (message.subtype === "success" && "result" in message) {
|
|
768
|
+
resultText = message.result as string;
|
|
798
769
|
}
|
|
799
770
|
}
|
|
800
771
|
}
|
|
801
772
|
|
|
802
|
-
|
|
803
|
-
|
|
773
|
+
if (!resultText) {
|
|
774
|
+
logger.debug("Curator v2: No result text received", "curator");
|
|
775
|
+
return { session_summary: "", memories: [] };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Log complete response for debugging
|
|
779
|
+
logger.debug(
|
|
780
|
+
`Curator v2: Complete response:\n${resultText}`,
|
|
781
|
+
"curator",
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// Use existing battle-tested parser
|
|
785
|
+
const result = this.parseCurationResponse(resultText);
|
|
786
|
+
|
|
787
|
+
logger.debug(
|
|
788
|
+
`Curator v2: Parsed ${result.memories.length} memories`,
|
|
789
|
+
"curator",
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
return result;
|
|
804
793
|
|
|
805
794
|
} catch (error: any) {
|
|
806
795
|
logger.debug(
|
|
@@ -812,49 +801,6 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
812
801
|
}
|
|
813
802
|
}
|
|
814
803
|
|
|
815
|
-
/**
|
|
816
|
-
* Convert Zod-validated result to our CurationResult type
|
|
817
|
-
* Handles the slight differences between Zod schema and internal types
|
|
818
|
-
*/
|
|
819
|
-
private _zodResultToCurationResult(zodResult: ZodCurationResult): CurationResult {
|
|
820
|
-
return {
|
|
821
|
-
session_summary: zodResult.session_summary,
|
|
822
|
-
interaction_tone: zodResult.interaction_tone ?? undefined,
|
|
823
|
-
project_snapshot: zodResult.project_snapshot
|
|
824
|
-
? {
|
|
825
|
-
id: "",
|
|
826
|
-
session_id: "",
|
|
827
|
-
project_id: "",
|
|
828
|
-
current_phase: zodResult.project_snapshot.current_phase,
|
|
829
|
-
recent_achievements: zodResult.project_snapshot.recent_achievements,
|
|
830
|
-
active_challenges: zodResult.project_snapshot.active_challenges,
|
|
831
|
-
next_steps: zodResult.project_snapshot.next_steps,
|
|
832
|
-
created_at: Date.now(),
|
|
833
|
-
}
|
|
834
|
-
: undefined,
|
|
835
|
-
memories: zodResult.memories.map((m) => ({
|
|
836
|
-
headline: m.headline,
|
|
837
|
-
content: m.content,
|
|
838
|
-
reasoning: m.reasoning,
|
|
839
|
-
importance_weight: m.importance_weight,
|
|
840
|
-
confidence_score: m.confidence_score,
|
|
841
|
-
context_type: m.context_type as ContextType,
|
|
842
|
-
temporal_class: m.temporal_class ?? "medium_term",
|
|
843
|
-
scope: m.scope,
|
|
844
|
-
trigger_phrases: m.trigger_phrases,
|
|
845
|
-
semantic_tags: m.semantic_tags,
|
|
846
|
-
question_types: [], // Not in Zod schema, will be filled by store
|
|
847
|
-
domain: m.domain,
|
|
848
|
-
feature: m.feature,
|
|
849
|
-
related_files: m.related_files,
|
|
850
|
-
action_required: m.action_required ?? false,
|
|
851
|
-
problem_solution_pair: m.problem_solution_pair ?? false,
|
|
852
|
-
awaiting_implementation: m.awaiting_implementation,
|
|
853
|
-
awaiting_decision: m.awaiting_decision,
|
|
854
|
-
})),
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
804
|
/**
|
|
859
805
|
* Legacy method: Curate using Anthropic SDK with API key
|
|
860
806
|
* Kept for backwards compatibility
|
|
@@ -915,8 +861,9 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
915
861
|
}
|
|
916
862
|
|
|
917
863
|
/**
|
|
918
|
-
* Find and curate from a session file directly
|
|
864
|
+
* Find and curate from a session file directly (LEGACY - no segmentation)
|
|
919
865
|
* Uses SDK mode to avoid CLI output truncation issues
|
|
866
|
+
* @deprecated Use curateFromSessionFileWithSegments() for large sessions
|
|
920
867
|
*/
|
|
921
868
|
async curateFromSessionFile(
|
|
922
869
|
sessionId: string,
|
|
@@ -951,6 +898,200 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
951
898
|
return this.curateWithSDK(session.messages as any, triggerType);
|
|
952
899
|
}
|
|
953
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
|
+
|
|
954
1095
|
/**
|
|
955
1096
|
* Find the session file path given a session ID
|
|
956
1097
|
*/
|
|
@@ -1222,6 +1363,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1222
1363
|
|
|
1223
1364
|
/**
|
|
1224
1365
|
* Fallback to SDK mode when CLI mode fails (e.g., output truncation)
|
|
1366
|
+
* Now uses segmented approach for large sessions
|
|
1225
1367
|
*/
|
|
1226
1368
|
private async _fallbackToSDK(
|
|
1227
1369
|
sessionId: string,
|
|
@@ -1229,10 +1371,18 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1229
1371
|
cwd?: string,
|
|
1230
1372
|
): Promise<CurationResult> {
|
|
1231
1373
|
try {
|
|
1232
|
-
|
|
1374
|
+
// Use segmented approach - same as ingest command
|
|
1375
|
+
const result = await this.curateFromSessionFileWithSegments(
|
|
1233
1376
|
sessionId,
|
|
1234
1377
|
triggerType,
|
|
1235
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
|
+
},
|
|
1236
1386
|
);
|
|
1237
1387
|
if (result.memories.length > 0) {
|
|
1238
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
|
|