@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.4.11",
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.1",
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 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(
@@ -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 + structured outputs (v2)
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
- * - Structured outputs with Zod validation - no regex JSON parsing
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} with structured outputs`,
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 structured JSON output.",
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
- // Handle result message
759
+ // Get the result text
766
760
  if (message.type === "result") {
767
- if (message.subtype === "success" && message.structured_output) {
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
- logger.debug("Curator v2: No result message received", "curator");
803
- return { session_summary: "", memories: [] };
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
- const result = await this.curateFromSessionFile(
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(
@@ -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