@rlabs-inc/memory 0.5.2 → 0.5.8
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/dist/index.js +5115 -41504
- package/dist/index.mjs +1425 -10549
- package/dist/server/index.js +3779 -12801
- package/dist/server/index.mjs +3474 -12498
- package/package.json +1 -1
- package/src/core/curator.ts +140 -448
- package/src/core/manager.ts +336 -528
- package/src/server/index.ts +2 -1
- package/src/utils/paths.ts +191 -0
package/src/core/curator.ts
CHANGED
|
@@ -15,33 +15,7 @@ import type {
|
|
|
15
15
|
} from "../types/memory.ts";
|
|
16
16
|
import { logger } from "../utils/logger.ts";
|
|
17
17
|
import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get the correct Claude CLI command path
|
|
21
|
-
* Uses `which` for universal discovery across installation methods
|
|
22
|
-
*/
|
|
23
|
-
function getClaudeCommand(): string {
|
|
24
|
-
// 1. Check for explicit override
|
|
25
|
-
const envCommand = process.env.CURATOR_COMMAND;
|
|
26
|
-
if (envCommand) {
|
|
27
|
-
return envCommand;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
|
|
31
|
-
const result = Bun.spawnSync(["which", "claude"]);
|
|
32
|
-
if (result.exitCode === 0) {
|
|
33
|
-
return result.stdout.toString().trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 3. Legacy fallback - hardcoded native install path
|
|
37
|
-
const claudeLocal = join(homedir(), ".claude", "local", "claude");
|
|
38
|
-
if (existsSync(claudeLocal)) {
|
|
39
|
-
return claudeLocal;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 4. Last resort - assume it's in PATH
|
|
43
|
-
return "claude";
|
|
44
|
-
}
|
|
18
|
+
import { getClaudeCommand, getCuratorPromptPath } from "../utils/paths.ts";
|
|
45
19
|
|
|
46
20
|
/**
|
|
47
21
|
* Curator configuration
|
|
@@ -657,13 +631,13 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
657
631
|
`Curator SDK raw response (${resultText.length} chars):`,
|
|
658
632
|
"curator",
|
|
659
633
|
);
|
|
660
|
-
if (logger.isVerbose()) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
634
|
+
// if (logger.isVerbose()) {
|
|
635
|
+
// const preview =
|
|
636
|
+
// resultText.length > 3000
|
|
637
|
+
// ? resultText.slice(0, 3000) + "...[truncated]"
|
|
638
|
+
// : resultText;
|
|
639
|
+
// console.log(preview);
|
|
640
|
+
// }
|
|
667
641
|
|
|
668
642
|
return this.parseCurationResponse(resultText);
|
|
669
643
|
}
|
|
@@ -730,16 +704,17 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
730
704
|
const curationPrompt = this.buildCurationPrompt(triggerType);
|
|
731
705
|
|
|
732
706
|
logger.debug(
|
|
733
|
-
`Curator
|
|
707
|
+
`Curator Claude - Resuming session ${claudeSessionId}`,
|
|
734
708
|
"curator",
|
|
735
709
|
);
|
|
736
710
|
|
|
737
711
|
try {
|
|
738
712
|
const q = query({
|
|
739
|
-
prompt:
|
|
713
|
+
prompt:
|
|
714
|
+
"Curate memories from this session according to your system instructions. Return ONLY the JSON structure.",
|
|
740
715
|
options: {
|
|
741
716
|
resume: claudeSessionId,
|
|
742
|
-
|
|
717
|
+
systemPrompt: curationPrompt,
|
|
743
718
|
model: "claude-opus-4-5-20251101",
|
|
744
719
|
permissionMode: "bypassPermissions",
|
|
745
720
|
},
|
|
@@ -749,18 +724,22 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
749
724
|
let resultText = "";
|
|
750
725
|
for await (const message of q) {
|
|
751
726
|
// Track usage for debugging
|
|
752
|
-
if (
|
|
727
|
+
if (
|
|
728
|
+
message.type === "assistant" &&
|
|
729
|
+
"usage" in message &&
|
|
730
|
+
message.usage
|
|
731
|
+
) {
|
|
753
732
|
logger.debug(
|
|
754
|
-
`Curator
|
|
733
|
+
`Curator Claude - Tokens used - input: ${message.usage}, output: ${message.usage}`,
|
|
755
734
|
"curator",
|
|
756
735
|
);
|
|
757
736
|
}
|
|
758
737
|
|
|
759
738
|
// Get the result text
|
|
760
739
|
if (message.type === "result") {
|
|
761
|
-
if (message.subtype
|
|
740
|
+
if (message.subtype !== "success") {
|
|
762
741
|
logger.debug(
|
|
763
|
-
`Curator
|
|
742
|
+
`Curator Claude - Error result - ${JSON.stringify(message)}`,
|
|
764
743
|
"curator",
|
|
765
744
|
);
|
|
766
745
|
return { session_summary: "", memories: [] };
|
|
@@ -771,29 +750,28 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
771
750
|
}
|
|
772
751
|
|
|
773
752
|
if (!resultText) {
|
|
774
|
-
logger.debug("Curator
|
|
753
|
+
logger.debug("Curator Claude - No result text received", "curator");
|
|
775
754
|
return { session_summary: "", memories: [] };
|
|
776
755
|
}
|
|
777
756
|
|
|
778
757
|
// Log complete response for debugging
|
|
779
|
-
logger.debug(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
);
|
|
758
|
+
// logger.debug(
|
|
759
|
+
// `Curator Claude - Complete response:\n${resultText}`,
|
|
760
|
+
// "curator",
|
|
761
|
+
// );
|
|
783
762
|
|
|
784
763
|
// Use existing battle-tested parser
|
|
785
764
|
const result = this.parseCurationResponse(resultText);
|
|
786
765
|
|
|
787
766
|
logger.debug(
|
|
788
|
-
`Curator
|
|
767
|
+
`Curator Claude - Parsed ${result.memories.length} memories`,
|
|
789
768
|
"curator",
|
|
790
769
|
);
|
|
791
770
|
|
|
792
771
|
return result;
|
|
793
|
-
|
|
794
772
|
} catch (error: any) {
|
|
795
773
|
logger.debug(
|
|
796
|
-
`Curator
|
|
774
|
+
`Curator Claude - Session resume failed: ${error.message}`,
|
|
797
775
|
"curator",
|
|
798
776
|
);
|
|
799
777
|
// Return empty - caller should fall back to transcript-based curation
|
|
@@ -809,34 +787,40 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
809
787
|
async curateWithGeminiCLI(
|
|
810
788
|
sessionId: string,
|
|
811
789
|
triggerType: CurationTrigger = "session_end",
|
|
790
|
+
cwd?: string,
|
|
812
791
|
): Promise<CurationResult> {
|
|
813
792
|
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
814
793
|
const userMessage =
|
|
815
794
|
"This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
|
|
816
795
|
|
|
817
|
-
// Write system prompt to temp file
|
|
818
|
-
const tempPromptPath =
|
|
819
|
-
|
|
820
|
-
// Ensure directory exists
|
|
821
|
-
const tempDir = join(homedir(), ".local", "share", "memory");
|
|
822
|
-
if (!existsSync(tempDir)) {
|
|
823
|
-
const { mkdirSync } = await import("fs");
|
|
824
|
-
mkdirSync(tempDir, { recursive: true });
|
|
825
|
-
}
|
|
826
|
-
|
|
796
|
+
// Write system prompt to temp file (tmpdir always exists)
|
|
797
|
+
const tempPromptPath = getCuratorPromptPath();
|
|
827
798
|
await Bun.write(tempPromptPath, systemPrompt);
|
|
828
799
|
|
|
829
800
|
// Build CLI command
|
|
801
|
+
// Use --resume latest since SessionEnd hook fires immediately after session ends
|
|
830
802
|
const args = [
|
|
831
|
-
"--resume",
|
|
832
|
-
"
|
|
833
|
-
"
|
|
803
|
+
"--resume",
|
|
804
|
+
"latest",
|
|
805
|
+
"-p",
|
|
806
|
+
userMessage,
|
|
807
|
+
"--output-format",
|
|
808
|
+
"json",
|
|
834
809
|
];
|
|
835
810
|
|
|
836
|
-
logger.debug(
|
|
811
|
+
logger.debug(
|
|
812
|
+
`Curator Gemini - Spawning gemini CLI to resume latest session (triggered by ${sessionId})`,
|
|
813
|
+
"curator",
|
|
814
|
+
);
|
|
815
|
+
logger.debug(
|
|
816
|
+
`Curator Gemini - cwd = '${cwd}' | (type: ${typeof cwd})`,
|
|
817
|
+
"curator",
|
|
818
|
+
);
|
|
837
819
|
|
|
838
820
|
// Execute CLI with system prompt via environment variable
|
|
821
|
+
// Must run from original project directory so --resume latest finds correct session
|
|
839
822
|
const proc = Bun.spawn(["gemini", ...args], {
|
|
823
|
+
cwd: cwd || undefined,
|
|
840
824
|
env: {
|
|
841
825
|
...process.env,
|
|
842
826
|
MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
|
|
@@ -855,11 +839,14 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
855
839
|
|
|
856
840
|
logger.debug(`Curator Gemini: Exit code ${exitCode}`, "curator");
|
|
857
841
|
if (stderr && stderr.trim()) {
|
|
858
|
-
logger.debug(`Curator Gemini stderr: ${stderr}`, "curator");
|
|
842
|
+
logger.debug(`Curator Gemini - stderr: ${stderr}`, "curator");
|
|
859
843
|
}
|
|
860
844
|
|
|
861
845
|
if (exitCode !== 0) {
|
|
862
|
-
logger.debug(
|
|
846
|
+
logger.debug(
|
|
847
|
+
`Curator Gemini - Failed with exit code ${exitCode}`,
|
|
848
|
+
"curator",
|
|
849
|
+
);
|
|
863
850
|
return { session_summary: "", memories: [] };
|
|
864
851
|
}
|
|
865
852
|
|
|
@@ -868,10 +855,16 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
868
855
|
// We need to extract just the JSON object
|
|
869
856
|
try {
|
|
870
857
|
// Find the JSON object - it starts with { and we need to find the matching }
|
|
871
|
-
const jsonStart = stdout.indexOf(
|
|
858
|
+
const jsonStart = stdout.indexOf("{");
|
|
872
859
|
if (jsonStart === -1) {
|
|
873
|
-
logger.debug(
|
|
874
|
-
|
|
860
|
+
logger.debug(
|
|
861
|
+
"Curator Gemini - No JSON object found in output",
|
|
862
|
+
"curator",
|
|
863
|
+
);
|
|
864
|
+
// logger.debug(
|
|
865
|
+
// `Curator Gemini - Raw stdout: ${stdout.slice(0, 500)}`,
|
|
866
|
+
// "curator",
|
|
867
|
+
// );
|
|
875
868
|
return { session_summary: "", memories: [] };
|
|
876
869
|
}
|
|
877
870
|
|
|
@@ -879,8 +872,8 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
879
872
|
let braceCount = 0;
|
|
880
873
|
let jsonEnd = -1;
|
|
881
874
|
for (let i = jsonStart; i < stdout.length; i++) {
|
|
882
|
-
if (stdout[i] ===
|
|
883
|
-
if (stdout[i] ===
|
|
875
|
+
if (stdout[i] === "{") braceCount++;
|
|
876
|
+
if (stdout[i] === "}") braceCount--;
|
|
884
877
|
if (braceCount === 0) {
|
|
885
878
|
jsonEnd = i + 1;
|
|
886
879
|
break;
|
|
@@ -888,20 +881,35 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
888
881
|
}
|
|
889
882
|
|
|
890
883
|
if (jsonEnd === -1) {
|
|
891
|
-
logger.debug(
|
|
884
|
+
logger.debug(
|
|
885
|
+
"Curator Gemini - Could not find matching closing brace",
|
|
886
|
+
"curator",
|
|
887
|
+
);
|
|
892
888
|
return { session_summary: "", memories: [] };
|
|
893
889
|
}
|
|
894
890
|
|
|
895
891
|
const jsonStr = stdout.slice(jsonStart, jsonEnd);
|
|
896
|
-
logger.debug(
|
|
892
|
+
logger.debug(
|
|
893
|
+
`Curator Gemini - Extracted JSON (${jsonStr.length} chars) from position ${jsonStart} to ${jsonEnd}`,
|
|
894
|
+
"curator",
|
|
895
|
+
);
|
|
897
896
|
|
|
898
897
|
let geminiOutput;
|
|
899
898
|
try {
|
|
900
899
|
geminiOutput = JSON.parse(jsonStr);
|
|
901
|
-
logger.debug(
|
|
900
|
+
logger.debug(
|
|
901
|
+
`Curator Gemini - Parsed outer JSON successfully`,
|
|
902
|
+
"curator",
|
|
903
|
+
);
|
|
902
904
|
} catch (outerError: any) {
|
|
903
|
-
logger.debug(
|
|
904
|
-
|
|
905
|
+
logger.debug(
|
|
906
|
+
`Curator Gemini - Outer JSON parse failed: ${outerError.message}`,
|
|
907
|
+
"curator",
|
|
908
|
+
);
|
|
909
|
+
// logger.debug(
|
|
910
|
+
// `Curator Gemini - JSON string (first 500): ${jsonStr.slice(0, 500)}`,
|
|
911
|
+
// "curator",
|
|
912
|
+
// );
|
|
905
913
|
return { session_summary: "", memories: [] };
|
|
906
914
|
}
|
|
907
915
|
|
|
@@ -910,80 +918,52 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
910
918
|
const aiResponse = geminiOutput.response || "";
|
|
911
919
|
|
|
912
920
|
if (!aiResponse) {
|
|
913
|
-
logger.debug("Curator Gemini
|
|
921
|
+
logger.debug("Curator Gemini - No response field in output", "curator");
|
|
914
922
|
return { session_summary: "", memories: [] };
|
|
915
923
|
}
|
|
916
924
|
|
|
917
|
-
logger.debug(
|
|
925
|
+
logger.debug(
|
|
926
|
+
`Curator Gemini - Got response (${aiResponse.length} chars)`,
|
|
927
|
+
"curator",
|
|
928
|
+
);
|
|
918
929
|
|
|
919
930
|
// Remove markdown code blocks if present
|
|
920
931
|
let cleanResponse = aiResponse;
|
|
921
932
|
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
922
933
|
if (codeBlockMatch) {
|
|
923
934
|
cleanResponse = codeBlockMatch[1].trim();
|
|
924
|
-
logger.debug(
|
|
935
|
+
logger.debug(
|
|
936
|
+
`Curator Gemini - Extracted JSON from code block (${cleanResponse.length} chars)`,
|
|
937
|
+
"curator",
|
|
938
|
+
);
|
|
925
939
|
} else {
|
|
926
|
-
logger.debug(
|
|
940
|
+
logger.debug(
|
|
941
|
+
`Curator Gemini - No code block found, using raw response`,
|
|
942
|
+
"curator",
|
|
943
|
+
);
|
|
927
944
|
}
|
|
928
945
|
|
|
929
|
-
logger.debug(
|
|
946
|
+
logger.debug(
|
|
947
|
+
`Curator Gemini - Calling parseCurationResponse...`,
|
|
948
|
+
"curator",
|
|
949
|
+
);
|
|
930
950
|
// Use existing parser
|
|
931
951
|
const result = this.parseCurationResponse(cleanResponse);
|
|
932
|
-
logger.debug(
|
|
952
|
+
logger.debug(
|
|
953
|
+
`Curator Gemini - Parsed ${result.memories.length} memories`,
|
|
954
|
+
"curator",
|
|
955
|
+
);
|
|
933
956
|
return result;
|
|
934
957
|
} catch (error: any) {
|
|
935
958
|
logger.debug(`Curator Gemini: Parse error: ${error.message}`, "curator");
|
|
936
|
-
logger.debug(
|
|
959
|
+
// logger.debug(
|
|
960
|
+
// `Curator Gemini - Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`,
|
|
961
|
+
// "curator",
|
|
962
|
+
// );
|
|
937
963
|
return { session_summary: "", memories: [] };
|
|
938
964
|
}
|
|
939
965
|
}
|
|
940
966
|
|
|
941
|
-
/**
|
|
942
|
-
* Legacy method: Curate using Anthropic SDK with API key
|
|
943
|
-
* Kept for backwards compatibility
|
|
944
|
-
* @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
|
|
945
|
-
*/
|
|
946
|
-
async curateWithAnthropicSDK(
|
|
947
|
-
messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
|
|
948
|
-
triggerType: CurationTrigger = "session_end",
|
|
949
|
-
): Promise<CurationResult> {
|
|
950
|
-
if (!this._config.apiKey) {
|
|
951
|
-
throw new Error(
|
|
952
|
-
"API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.",
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Dynamic import to make SDK optional
|
|
957
|
-
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
958
|
-
const client = new Anthropic({ apiKey: this._config.apiKey });
|
|
959
|
-
|
|
960
|
-
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
961
|
-
|
|
962
|
-
// Build the conversation: original messages + curation request
|
|
963
|
-
const conversationMessages = [
|
|
964
|
-
...messages,
|
|
965
|
-
{
|
|
966
|
-
role: "user" as const,
|
|
967
|
-
content:
|
|
968
|
-
"This session has ended. Please curate the memories from our conversation according to your system instructions. Return ONLY the JSON structure with no additional text.",
|
|
969
|
-
},
|
|
970
|
-
];
|
|
971
|
-
|
|
972
|
-
const response = await client.messages.create({
|
|
973
|
-
model: "claude-sonnet-4-20250514",
|
|
974
|
-
max_tokens: 64000,
|
|
975
|
-
system: systemPrompt,
|
|
976
|
-
messages: conversationMessages,
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const content = response.content[0];
|
|
980
|
-
if (content.type !== "text") {
|
|
981
|
-
throw new Error("Unexpected response type from Claude API");
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
return this.parseCurationResponse(content.text);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
967
|
/**
|
|
988
968
|
* Curate from a parsed session segment
|
|
989
969
|
* Convenience method that extracts messages from SessionSegment
|
|
@@ -997,44 +977,6 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
997
977
|
return this.curateWithSDK(segment.messages, triggerType);
|
|
998
978
|
}
|
|
999
979
|
|
|
1000
|
-
/**
|
|
1001
|
-
* Find and curate from a session file directly (LEGACY - no segmentation)
|
|
1002
|
-
* Uses SDK mode to avoid CLI output truncation issues
|
|
1003
|
-
* @deprecated Use curateFromSessionFileWithSegments() for large sessions
|
|
1004
|
-
*/
|
|
1005
|
-
async curateFromSessionFile(
|
|
1006
|
-
sessionId: string,
|
|
1007
|
-
triggerType: CurationTrigger = "session_end",
|
|
1008
|
-
cwd?: string,
|
|
1009
|
-
): Promise<CurationResult> {
|
|
1010
|
-
// Find the session file
|
|
1011
|
-
const sessionFile = await this._findSessionFile(sessionId, cwd);
|
|
1012
|
-
if (!sessionFile) {
|
|
1013
|
-
logger.debug(
|
|
1014
|
-
`Curator: Could not find session file for ${sessionId}`,
|
|
1015
|
-
"curator",
|
|
1016
|
-
);
|
|
1017
|
-
return { session_summary: "", memories: [] };
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
|
|
1021
|
-
|
|
1022
|
-
// Parse the session
|
|
1023
|
-
const session = await parseSessionFile(sessionFile);
|
|
1024
|
-
if (session.messages.length === 0) {
|
|
1025
|
-
logger.debug("Curator: Session has no messages", "curator");
|
|
1026
|
-
return { session_summary: "", memories: [] };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
logger.debug(
|
|
1030
|
-
`Curator: Parsed ${session.messages.length} messages, ~${session.metadata.estimatedTokens} tokens`,
|
|
1031
|
-
"curator",
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
// Use SDK mode with the parsed messages
|
|
1035
|
-
return this.curateWithSDK(session.messages as any, triggerType);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
980
|
/**
|
|
1039
981
|
* Find and curate from a session file with SEGMENTATION
|
|
1040
982
|
* Breaks large sessions into segments and curates each one
|
|
@@ -1062,43 +1004,50 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1062
1004
|
const sessionFile = await this._findSessionFile(sessionId, cwd);
|
|
1063
1005
|
if (!sessionFile) {
|
|
1064
1006
|
logger.debug(
|
|
1065
|
-
`Curator
|
|
1007
|
+
`Curator Claude - Could not find session file for ${sessionId}`,
|
|
1066
1008
|
"curator",
|
|
1067
1009
|
);
|
|
1068
1010
|
return { session_summary: "", memories: [] };
|
|
1069
1011
|
}
|
|
1070
1012
|
|
|
1071
|
-
logger.debug(
|
|
1013
|
+
logger.debug(
|
|
1014
|
+
`Curator Claude - Found session file: ${sessionFile}`,
|
|
1015
|
+
"curator",
|
|
1016
|
+
);
|
|
1072
1017
|
|
|
1073
1018
|
// Parse the session to get metadata first
|
|
1074
1019
|
const session = await parseSessionFile(sessionFile);
|
|
1075
1020
|
if (session.messages.length === 0) {
|
|
1076
|
-
logger.debug("Curator
|
|
1021
|
+
logger.debug("Curator Claude - Session has no messages", "curator");
|
|
1077
1022
|
return { session_summary: "", memories: [] };
|
|
1078
1023
|
}
|
|
1079
1024
|
|
|
1080
1025
|
// Log detailed session stats
|
|
1081
1026
|
const { metadata } = session;
|
|
1082
1027
|
logger.debug(
|
|
1083
|
-
`Curator
|
|
1028
|
+
`Curator Claude - Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
|
|
1084
1029
|
"curator",
|
|
1085
1030
|
);
|
|
1086
1031
|
logger.debug(
|
|
1087
|
-
`Curator
|
|
1032
|
+
`Curator Claude - Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
|
|
1088
1033
|
"curator",
|
|
1089
1034
|
);
|
|
1090
1035
|
|
|
1091
1036
|
// Parse into segments using the same function as ingest
|
|
1092
|
-
const { parseSessionFileWithSegments } =
|
|
1093
|
-
|
|
1037
|
+
const { parseSessionFileWithSegments } =
|
|
1038
|
+
await import("./session-parser.ts");
|
|
1039
|
+
const segments = await parseSessionFileWithSegments(
|
|
1040
|
+
sessionFile,
|
|
1041
|
+
maxTokensPerSegment,
|
|
1042
|
+
);
|
|
1094
1043
|
|
|
1095
1044
|
if (segments.length === 0) {
|
|
1096
|
-
logger.debug("Curator
|
|
1045
|
+
logger.debug("Curator Claude - No segments found in session", "curator");
|
|
1097
1046
|
return { session_summary: "", memories: [] };
|
|
1098
1047
|
}
|
|
1099
1048
|
|
|
1100
1049
|
logger.debug(
|
|
1101
|
-
`Curator
|
|
1050
|
+
`Curator Claude - Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
|
|
1102
1051
|
"curator",
|
|
1103
1052
|
);
|
|
1104
1053
|
|
|
@@ -1106,7 +1055,8 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1106
1055
|
const allMemories: CuratedMemory[] = [];
|
|
1107
1056
|
const sessionSummaries: string[] = [];
|
|
1108
1057
|
const interactionTones: string[] = [];
|
|
1109
|
-
const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] =
|
|
1058
|
+
const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] =
|
|
1059
|
+
[];
|
|
1110
1060
|
let failedSegments = 0;
|
|
1111
1061
|
|
|
1112
1062
|
// Curate each segment
|
|
@@ -1115,7 +1065,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1115
1065
|
const tokensLabel = `${Math.round(segment.estimatedTokens / 1000)}k`;
|
|
1116
1066
|
|
|
1117
1067
|
logger.debug(
|
|
1118
|
-
`Curator
|
|
1068
|
+
`Curator Claude - Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
|
|
1119
1069
|
"curator",
|
|
1120
1070
|
);
|
|
1121
1071
|
|
|
@@ -1138,7 +1088,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1138
1088
|
}
|
|
1139
1089
|
|
|
1140
1090
|
logger.debug(
|
|
1141
|
-
`Curator
|
|
1091
|
+
`Curator Claude - Segment ${segmentLabel} extracted ${result.memories.length} memories`,
|
|
1142
1092
|
"curator",
|
|
1143
1093
|
);
|
|
1144
1094
|
|
|
@@ -1154,7 +1104,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1154
1104
|
} catch (error: any) {
|
|
1155
1105
|
failedSegments++;
|
|
1156
1106
|
logger.debug(
|
|
1157
|
-
`Curator
|
|
1107
|
+
`Curator Claude - Segment ${segmentLabel} failed: ${error.message}`,
|
|
1158
1108
|
"curator",
|
|
1159
1109
|
);
|
|
1160
1110
|
}
|
|
@@ -1163,16 +1113,16 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1163
1113
|
// Log final summary
|
|
1164
1114
|
if (failedSegments > 0) {
|
|
1165
1115
|
logger.debug(
|
|
1166
|
-
`Curator
|
|
1116
|
+
`Curator Claude - Completed with ${failedSegments} failed segment(s)`,
|
|
1167
1117
|
"curator",
|
|
1168
1118
|
);
|
|
1169
1119
|
}
|
|
1170
1120
|
logger.debug(
|
|
1171
|
-
`Curator
|
|
1121
|
+
`Curator Claude - Total ${allMemories.length} memories from ${segments.length} segment(s)`,
|
|
1172
1122
|
"curator",
|
|
1173
1123
|
);
|
|
1174
1124
|
logger.debug(
|
|
1175
|
-
`Curator
|
|
1125
|
+
`Curator Claude - Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
|
|
1176
1126
|
"curator",
|
|
1177
1127
|
);
|
|
1178
1128
|
|
|
@@ -1189,9 +1139,10 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1189
1139
|
}
|
|
1190
1140
|
|
|
1191
1141
|
// For interaction tone, use the most common one or the last one
|
|
1192
|
-
const finalTone =
|
|
1193
|
-
|
|
1194
|
-
|
|
1142
|
+
const finalTone =
|
|
1143
|
+
interactionTones.length > 0
|
|
1144
|
+
? interactionTones[interactionTones.length - 1]
|
|
1145
|
+
: undefined;
|
|
1195
1146
|
|
|
1196
1147
|
// For project snapshot, merge all snapshots - later ones take precedence for phase,
|
|
1197
1148
|
// but accumulate achievements/challenges/next_steps
|
|
@@ -1202,8 +1153,10 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1202
1153
|
const allNextSteps: string[] = [];
|
|
1203
1154
|
|
|
1204
1155
|
for (const snap of projectSnapshots) {
|
|
1205
|
-
if (snap.recent_achievements)
|
|
1206
|
-
|
|
1156
|
+
if (snap.recent_achievements)
|
|
1157
|
+
allAchievements.push(...snap.recent_achievements);
|
|
1158
|
+
if (snap.active_challenges)
|
|
1159
|
+
allChallenges.push(...snap.active_challenges);
|
|
1207
1160
|
if (snap.next_steps) allNextSteps.push(...snap.next_steps);
|
|
1208
1161
|
}
|
|
1209
1162
|
|
|
@@ -1277,267 +1230,6 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
1277
1230
|
|
|
1278
1231
|
return null;
|
|
1279
1232
|
}
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Curate using CLI subprocess (for hook mode)
|
|
1283
|
-
* Resumes a session and asks it to curate
|
|
1284
|
-
*/
|
|
1285
|
-
async curateWithCLI(
|
|
1286
|
-
sessionId: string,
|
|
1287
|
-
triggerType: CurationTrigger = "session_end",
|
|
1288
|
-
cwd?: string,
|
|
1289
|
-
cliTypeOverride?: "claude-code" | "gemini-cli",
|
|
1290
|
-
): Promise<CurationResult> {
|
|
1291
|
-
const type = cliTypeOverride ?? this._config.cliType;
|
|
1292
|
-
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
1293
|
-
const userMessage =
|
|
1294
|
-
"This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
|
|
1295
|
-
|
|
1296
|
-
// Build CLI command based on type
|
|
1297
|
-
const args: string[] = [];
|
|
1298
|
-
let command = this._config.cliCommand;
|
|
1299
|
-
|
|
1300
|
-
if (type === "claude-code") {
|
|
1301
|
-
args.push(
|
|
1302
|
-
"--resume",
|
|
1303
|
-
sessionId,
|
|
1304
|
-
"-p",
|
|
1305
|
-
userMessage,
|
|
1306
|
-
"--append-system-prompt",
|
|
1307
|
-
systemPrompt,
|
|
1308
|
-
"--output-format",
|
|
1309
|
-
"json",
|
|
1310
|
-
);
|
|
1311
|
-
} else {
|
|
1312
|
-
// gemini-cli
|
|
1313
|
-
command = "gemini"; // Default to 'gemini' in PATH for gemini-cli
|
|
1314
|
-
args.push(
|
|
1315
|
-
"--resume",
|
|
1316
|
-
sessionId,
|
|
1317
|
-
"-p",
|
|
1318
|
-
`${systemPrompt}\n\n${userMessage}`,
|
|
1319
|
-
"--output-format",
|
|
1320
|
-
"json",
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// Execute CLI
|
|
1325
|
-
logger.debug(
|
|
1326
|
-
`Curator: Spawning CLI with CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000`,
|
|
1327
|
-
"curator",
|
|
1328
|
-
);
|
|
1329
|
-
logger.debug(
|
|
1330
|
-
`Curator: Command: ${command} ${args.slice(0, 3).join(" ")}...`,
|
|
1331
|
-
"curator",
|
|
1332
|
-
);
|
|
1333
|
-
|
|
1334
|
-
const proc = Bun.spawn([command, ...args], {
|
|
1335
|
-
cwd,
|
|
1336
|
-
env: {
|
|
1337
|
-
...process.env,
|
|
1338
|
-
MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
|
|
1339
|
-
CLAUDE_CODE_MAX_OUTPUT_TOKENS: "64000", // Max output to avoid truncation
|
|
1340
|
-
},
|
|
1341
|
-
stdout: "pipe",
|
|
1342
|
-
stderr: "pipe",
|
|
1343
|
-
});
|
|
1344
|
-
|
|
1345
|
-
// Capture both stdout and stderr
|
|
1346
|
-
const [stdout, stderr] = await Promise.all([
|
|
1347
|
-
new Response(proc.stdout).text(),
|
|
1348
|
-
new Response(proc.stderr).text(),
|
|
1349
|
-
]);
|
|
1350
|
-
const exitCode = await proc.exited;
|
|
1351
|
-
|
|
1352
|
-
logger.debug(`Curator CLI exit code: ${exitCode}`, "curator");
|
|
1353
|
-
if (stderr && stderr.trim()) {
|
|
1354
|
-
logger.debug(
|
|
1355
|
-
`Curator stderr (${stderr.length} chars): ${stderr}`,
|
|
1356
|
-
"curator",
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
if (exitCode !== 0) {
|
|
1361
|
-
return { session_summary: "", memories: [] };
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// Log raw response in verbose mode
|
|
1365
|
-
logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, "curator");
|
|
1366
|
-
// Always log the last 100 chars to see where output ends
|
|
1367
|
-
logger.debug(`Curator: '${stdout}'`, "curator");
|
|
1368
|
-
if (logger.isVerbose()) {
|
|
1369
|
-
// Show first 2000 chars to avoid flooding console
|
|
1370
|
-
const preview = stdout.length > 2000 ? stdout : stdout;
|
|
1371
|
-
console.log(preview);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// Extract JSON from CLI output
|
|
1375
|
-
try {
|
|
1376
|
-
// First, parse the CLI JSON wrapper
|
|
1377
|
-
const cliOutput = JSON.parse(stdout);
|
|
1378
|
-
|
|
1379
|
-
// Claude Code now returns an array of events - find the result object
|
|
1380
|
-
let resultObj: any;
|
|
1381
|
-
if (Array.isArray(cliOutput)) {
|
|
1382
|
-
// New format: array of events, find the one with type="result"
|
|
1383
|
-
resultObj = cliOutput.find((item: any) => item.type === "result");
|
|
1384
|
-
if (!resultObj) {
|
|
1385
|
-
logger.debug(
|
|
1386
|
-
"Curator: No result object found in CLI output array",
|
|
1387
|
-
"curator",
|
|
1388
|
-
);
|
|
1389
|
-
return { session_summary: "", memories: [] };
|
|
1390
|
-
}
|
|
1391
|
-
} else {
|
|
1392
|
-
// Old format: single object (backwards compatibility)
|
|
1393
|
-
resultObj = cliOutput;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// Check for error response FIRST (like Python does)
|
|
1397
|
-
if (resultObj.type === "error" || resultObj.is_error === true) {
|
|
1398
|
-
logger.debug(
|
|
1399
|
-
`Curator: Error response from CLI: ${JSON.stringify(resultObj)}`,
|
|
1400
|
-
"curator",
|
|
1401
|
-
);
|
|
1402
|
-
return { session_summary: "", memories: [] };
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// Extract the "result" field (AI's response text)
|
|
1406
|
-
let aiResponse = "";
|
|
1407
|
-
if (typeof resultObj.result === "string") {
|
|
1408
|
-
aiResponse = resultObj.result;
|
|
1409
|
-
} else {
|
|
1410
|
-
logger.debug(
|
|
1411
|
-
`Curator: result field is not a string: ${typeof resultObj.result}`,
|
|
1412
|
-
"curator",
|
|
1413
|
-
);
|
|
1414
|
-
return { session_summary: "", memories: [] };
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// Log the AI response in verbose mode
|
|
1418
|
-
logger.debug(
|
|
1419
|
-
`Curator AI response (${aiResponse.length} chars):`,
|
|
1420
|
-
"curator",
|
|
1421
|
-
);
|
|
1422
|
-
if (logger.isVerbose()) {
|
|
1423
|
-
const preview = aiResponse.length > 3000 ? aiResponse : aiResponse;
|
|
1424
|
-
console.log(preview);
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// Remove markdown code blocks if present (```json ... ```)
|
|
1428
|
-
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1429
|
-
if (codeBlockMatch) {
|
|
1430
|
-
logger.debug(
|
|
1431
|
-
`Curator: Code block matched, extracting ${codeBlockMatch[1]!.length} chars`,
|
|
1432
|
-
"curator",
|
|
1433
|
-
);
|
|
1434
|
-
aiResponse = codeBlockMatch[1]!.trim();
|
|
1435
|
-
} else {
|
|
1436
|
-
logger.debug(
|
|
1437
|
-
`Curator: No code block found, using raw response`,
|
|
1438
|
-
"curator",
|
|
1439
|
-
);
|
|
1440
|
-
// Log the last 200 chars to see where truncation happened
|
|
1441
|
-
if (aiResponse.length > 200) {
|
|
1442
|
-
logger.debug(`Curator: ${aiResponse}`, "curator");
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Now find the JSON object (same regex as Python)
|
|
1447
|
-
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0];
|
|
1448
|
-
if (jsonMatch) {
|
|
1449
|
-
logger.debug(
|
|
1450
|
-
`Curator: Found JSON object (${jsonMatch.length} chars), parsing...`,
|
|
1451
|
-
"curator",
|
|
1452
|
-
);
|
|
1453
|
-
|
|
1454
|
-
// Detect likely truncation: JSON much smaller than response
|
|
1455
|
-
const likelyTruncated = jsonMatch.length < aiResponse.length * 0.5;
|
|
1456
|
-
|
|
1457
|
-
if (likelyTruncated) {
|
|
1458
|
-
logger.debug(
|
|
1459
|
-
`Curator: WARNING - JSON (${jsonMatch.length}) much smaller than response (${aiResponse.length}) - likely truncated`,
|
|
1460
|
-
"curator",
|
|
1461
|
-
);
|
|
1462
|
-
// Find the last } position and log what's around it
|
|
1463
|
-
const lastBrace = aiResponse.lastIndexOf("}");
|
|
1464
|
-
logger.debug(
|
|
1465
|
-
`Curator: Last } at position ${lastBrace}, char before: '${aiResponse[lastBrace - 1]}', char after: '${aiResponse[lastBrace + 1] || "EOF"}'`,
|
|
1466
|
-
"curator",
|
|
1467
|
-
);
|
|
1468
|
-
// Log chars around the cut point
|
|
1469
|
-
const cutPoint = jsonMatch.length;
|
|
1470
|
-
logger.debug(
|
|
1471
|
-
`Curator: Around match end (${cutPoint}): '...${aiResponse.slice(Math.max(0, cutPoint - 50), cutPoint + 50)}...'`,
|
|
1472
|
-
"curator",
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
const result = this.parseCurationResponse(jsonMatch);
|
|
1477
|
-
|
|
1478
|
-
// If we got 0 memories and likely truncated, try SDK fallback
|
|
1479
|
-
if (result.memories.length === 0 && likelyTruncated) {
|
|
1480
|
-
logger.debug(
|
|
1481
|
-
"Curator: CLI mode returned 0 memories with truncation detected, trying SDK fallback...",
|
|
1482
|
-
"curator",
|
|
1483
|
-
);
|
|
1484
|
-
return this._fallbackToSDK(sessionId, triggerType, cwd);
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
return result;
|
|
1488
|
-
} else {
|
|
1489
|
-
logger.debug("Curator: No JSON object found in AI response", "curator");
|
|
1490
|
-
}
|
|
1491
|
-
} catch (error: any) {
|
|
1492
|
-
// Parse error - return empty result
|
|
1493
|
-
logger.debug(`Curator: Parse error: ${error.message}`, "curator");
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// CLI mode failed - try SDK fallback
|
|
1497
|
-
logger.debug("Curator: CLI mode failed, trying SDK fallback...", "curator");
|
|
1498
|
-
return this._fallbackToSDK(sessionId, triggerType, cwd);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
/**
|
|
1502
|
-
* Fallback to SDK mode when CLI mode fails (e.g., output truncation)
|
|
1503
|
-
* Now uses segmented approach for large sessions
|
|
1504
|
-
*/
|
|
1505
|
-
private async _fallbackToSDK(
|
|
1506
|
-
sessionId: string,
|
|
1507
|
-
triggerType: CurationTrigger,
|
|
1508
|
-
cwd?: string,
|
|
1509
|
-
): Promise<CurationResult> {
|
|
1510
|
-
try {
|
|
1511
|
-
// Use segmented approach - same as ingest command
|
|
1512
|
-
const result = await this.curateFromSessionFileWithSegments(
|
|
1513
|
-
sessionId,
|
|
1514
|
-
triggerType,
|
|
1515
|
-
cwd,
|
|
1516
|
-
150000, // 150k tokens per segment
|
|
1517
|
-
(progress) => {
|
|
1518
|
-
logger.debug(
|
|
1519
|
-
`Curator fallback: Segment ${progress.segmentIndex + 1}/${progress.totalSegments} → ${progress.memoriesExtracted} memories`,
|
|
1520
|
-
"curator",
|
|
1521
|
-
);
|
|
1522
|
-
},
|
|
1523
|
-
);
|
|
1524
|
-
if (result.memories.length > 0) {
|
|
1525
|
-
logger.debug(
|
|
1526
|
-
`Curator: SDK fallback succeeded with ${result.memories.length} memories`,
|
|
1527
|
-
"curator",
|
|
1528
|
-
);
|
|
1529
|
-
} else {
|
|
1530
|
-
logger.debug(
|
|
1531
|
-
"Curator: SDK fallback also returned 0 memories",
|
|
1532
|
-
"curator",
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
return result;
|
|
1536
|
-
} catch (error: any) {
|
|
1537
|
-
logger.debug(`Curator: SDK fallback failed: ${error.message}`, "curator");
|
|
1538
|
-
return { session_summary: "", memories: [] };
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
1233
|
}
|
|
1542
1234
|
|
|
1543
1235
|
/**
|