@rlabs-inc/memory 0.4.9 → 0.4.11

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.9",
3
+ "version": "0.4.11",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,8 @@
42
42
  "@anthropic-ai/claude-agent-sdk": "^0.2.1",
43
43
  "@huggingface/transformers": "^3.4.1",
44
44
  "@rlabs-inc/fsdb": "^1.0.1",
45
- "@rlabs-inc/signals": "^1.0.0"
45
+ "@rlabs-inc/signals": "^1.0.0",
46
+ "zod": "^4.3.5"
46
47
  },
47
48
  "devDependencies": {
48
49
  "typescript": "^5.0.0",
@@ -39,21 +39,23 @@ async function installClaudeHooks(options: InstallOptions) {
39
39
  console.log()
40
40
 
41
41
  const claudeDir = join(homedir(), '.claude')
42
+ const targetHooksDir = join(claudeDir, 'hooks')
42
43
  const settingsPath = join(claudeDir, 'settings.json')
43
44
 
44
- // Find the hooks directory (relative to this CLI)
45
+ // Find the hooks directory (relative to this CLI - source files)
45
46
  const cliPath = import.meta.dir
46
47
  const packageRoot = join(cliPath, '..', '..', '..')
47
- const hooksDir = join(packageRoot, 'hooks', 'claude')
48
+ const sourceHooksDir = join(packageRoot, 'hooks', 'claude')
48
49
 
49
50
  console.log(` ${fmt.kv('Claude config', claudeDir)}`)
50
- console.log(` ${fmt.kv('Hooks source', hooksDir)}`)
51
+ console.log(` ${fmt.kv('Hooks source', sourceHooksDir)}`)
52
+ console.log(` ${fmt.kv('Hooks target', targetHooksDir)}`)
51
53
  console.log()
52
54
 
53
- // Check if hooks directory exists
54
- if (!existsSync(hooksDir)) {
55
+ // Check if source hooks directory exists
56
+ if (!existsSync(sourceHooksDir)) {
55
57
  console.log(
56
- c.error(` ${symbols.cross} Hooks directory not found at ${hooksDir}`)
58
+ c.error(` ${symbols.cross} Hooks directory not found at ${sourceHooksDir}`)
57
59
  )
58
60
  console.log(c.muted(` Make sure the memory package is properly installed`))
59
61
  process.exit(1)
@@ -65,6 +67,28 @@ async function installClaudeHooks(options: InstallOptions) {
65
67
  console.log(` ${c.success(symbols.tick)} Created ${claudeDir}`)
66
68
  }
67
69
 
70
+ // Ensure target hooks directory exists
71
+ if (!existsSync(targetHooksDir)) {
72
+ mkdirSync(targetHooksDir, { recursive: true })
73
+ console.log(` ${c.success(symbols.tick)} Created ${targetHooksDir}`)
74
+ }
75
+
76
+ // Copy hooks to target directory (stable location, won't change with package upgrades)
77
+ const filesToCopy = ['session-start.ts', 'user-prompt.ts', 'curation.ts']
78
+ for (const file of filesToCopy) {
79
+ const source = join(sourceHooksDir, file)
80
+ const target = join(targetHooksDir, file)
81
+ try {
82
+ const content = await Bun.file(source).text()
83
+ await Bun.write(target, content)
84
+ console.log(` ${c.success(symbols.tick)} Installed hook: ${file}`)
85
+ } catch (e: any) {
86
+ console.log(
87
+ c.error(` ${symbols.cross} Failed to copy ${file}: ${e.message}`)
88
+ )
89
+ }
90
+ }
91
+
68
92
  // Read existing settings or create new
69
93
  let settings: any = {}
70
94
  if (existsSync(settingsPath)) {
@@ -83,10 +107,10 @@ async function installClaudeHooks(options: InstallOptions) {
83
107
  }
84
108
  }
85
109
 
86
- // Build hooks configuration
87
- const sessionStartHook = join(hooksDir, 'session-start.ts')
88
- const userPromptHook = join(hooksDir, 'user-prompt.ts')
89
- const curationHook = join(hooksDir, 'curation.ts')
110
+ // Build hooks configuration pointing to TARGET directory (stable ~/.claude/hooks/)
111
+ const sessionStartHook = join(targetHooksDir, 'session-start.ts')
112
+ const userPromptHook = join(targetHooksDir, 'user-prompt.ts')
113
+ const curationHook = join(targetHooksDir, 'curation.ts')
90
114
 
91
115
  const hooksConfig = {
92
116
  SessionStart: [
@@ -14,6 +14,8 @@ 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";
17
19
  import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
18
20
 
19
21
  /**
@@ -709,6 +711,150 @@ This session has ended. Please curate the memories from this conversation accord
709
711
  return lines.join("\n");
710
712
  }
711
713
 
714
+ /**
715
+ * Curate using session resumption + structured outputs (v2)
716
+ *
717
+ * This is the preferred method when we have a Claude session ID.
718
+ * Benefits over transcript parsing:
719
+ * - 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
722
+ *
723
+ * @param claudeSessionId - The actual Claude Code session ID (resumable)
724
+ * @param triggerType - What triggered curation (session_end, pre_compact, etc.)
725
+ */
726
+ async curateWithSessionResume(
727
+ claudeSessionId: string,
728
+ triggerType: CurationTrigger = "session_end",
729
+ ): Promise<CurationResult> {
730
+ // Dynamic import to make Agent SDK optional
731
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
732
+
733
+ const curationPrompt = this.buildCurationPrompt(triggerType);
734
+ const jsonSchema = z.toJSONSchema(CurationResultSchema);
735
+
736
+ logger.debug(
737
+ `Curator v2: Resuming session ${claudeSessionId} with structured outputs`,
738
+ "curator",
739
+ );
740
+
741
+ try {
742
+ const q = query({
743
+ prompt: "Curate memories from this session according to your system instructions. Return the structured JSON output.",
744
+ options: {
745
+ resume: claudeSessionId,
746
+ appendSystemPrompt: curationPrompt, // APPEND, don't replace!
747
+ model: "claude-opus-4-5-20251101",
748
+ permissionMode: "bypassPermissions",
749
+ outputFormat: {
750
+ type: "json_schema",
751
+ schema: jsonSchema,
752
+ },
753
+ },
754
+ });
755
+
756
+ for await (const message of q) {
757
+ // Track usage for debugging
758
+ if (message.type === "assistant" && "usage" in message && message.usage) {
759
+ logger.debug(
760
+ `Curator v2: Tokens used - input: ${message.usage.input_tokens}, output: ${message.usage.output_tokens}`,
761
+ "curator",
762
+ );
763
+ }
764
+
765
+ // Handle result message
766
+ 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") {
793
+ logger.debug(
794
+ `Curator v2: Error result - ${JSON.stringify(message)}`,
795
+ "curator",
796
+ );
797
+ return { session_summary: "", memories: [] };
798
+ }
799
+ }
800
+ }
801
+
802
+ logger.debug("Curator v2: No result message received", "curator");
803
+ return { session_summary: "", memories: [] };
804
+
805
+ } catch (error: any) {
806
+ logger.debug(
807
+ `Curator v2: Session resume failed: ${error.message}`,
808
+ "curator",
809
+ );
810
+ // Return empty - caller should fall back to transcript-based curation
811
+ return { session_summary: "", memories: [] };
812
+ }
813
+ }
814
+
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
+
712
858
  /**
713
859
  * Legacy method: Curate using Anthropic SDK with API key
714
860
  * Kept for backwards compatibility
@@ -197,13 +197,23 @@ export async function createServer(config: ServerConfig = {}) {
197
197
  // Fire and forget - don't block the response
198
198
  setImmediate(async () => {
199
199
  try {
200
- const result = await curator.curateWithCLI(
200
+ // Try session resume first (v2) - gets full context including tool uses
201
+ // Falls back to transcript parsing if resume fails
202
+ let result = await curator.curateWithSessionResume(
201
203
  body.claude_session_id,
202
- body.trigger,
203
- body.cwd,
204
- body.cli_type
204
+ body.trigger
205
205
  )
206
206
 
207
+ // Fallback to transcript-based curation if resume returned nothing
208
+ if (result.memories.length === 0) {
209
+ logger.debug('Session resume returned no memories, falling back to transcript parsing', 'server')
210
+ result = await curator.curateFromSessionFile(
211
+ body.claude_session_id,
212
+ body.trigger,
213
+ body.cwd
214
+ )
215
+ }
216
+
207
217
  if (result.memories.length > 0) {
208
218
  await engine.storeCurationResult(
209
219
  body.project_id,
@@ -225,7 +235,8 @@ export async function createServer(config: ServerConfig = {}) {
225
235
  logger.logManagementStart(result.memories.length)
226
236
  const startTime = Date.now()
227
237
 
228
- const managementResult = await manager.manageWithCLI(
238
+ // Use SDK mode - more reliable than CLI which can go off-rails
239
+ const managementResult = await manager.manageWithSDK(
229
240
  body.project_id,
230
241
  sessionNumber,
231
242
  result,
@@ -0,0 +1,107 @@
1
+ // ============================================================================
2
+ // CURATION SCHEMA - Zod schemas for SDK structured outputs
3
+ // Mirrors memory.ts types for JSON Schema generation
4
+ // ============================================================================
5
+
6
+ import { z } from 'zod'
7
+
8
+ /**
9
+ * All 11 canonical context types - matches memory.ts CONTEXT_TYPES
10
+ */
11
+ export const ContextTypeSchema = z.enum([
12
+ 'technical',
13
+ 'debug',
14
+ 'architecture',
15
+ 'decision',
16
+ 'personal',
17
+ 'philosophy',
18
+ 'workflow',
19
+ 'milestone',
20
+ 'breakthrough',
21
+ 'unresolved',
22
+ 'state'
23
+ ])
24
+
25
+ /**
26
+ * Temporal class - matches memory.ts TemporalClass
27
+ */
28
+ export const TemporalClassSchema = z.enum([
29
+ 'eternal',
30
+ 'long_term',
31
+ 'medium_term',
32
+ 'short_term',
33
+ 'ephemeral'
34
+ ])
35
+
36
+ /**
37
+ * Scope - global (shared) or project-specific
38
+ */
39
+ export const ScopeSchema = z.enum(['global', 'project'])
40
+
41
+ /**
42
+ * Single curated memory - matches memory.ts CuratedMemory
43
+ * Fields marked optional have smart defaults applied by applyV4Defaults()
44
+ */
45
+ export const CuratedMemorySchema = z.object({
46
+ // Core content (v4: two-tier structure)
47
+ headline: z.string().describe('1-2 line summary WITH conclusion - always shown in retrieval'),
48
+ content: z.string().describe('Full structured template (WHAT/WHERE/HOW/WHY) - expandable'),
49
+ reasoning: z.string().describe('Why this memory matters for future sessions'),
50
+
51
+ // Scores
52
+ importance_weight: z.number().min(0).max(1).describe('0.9+ breakthrough, 0.7-0.8 important, 0.5-0.6 useful'),
53
+ confidence_score: z.number().min(0).max(1).describe('How confident in this assessment'),
54
+
55
+ // Classification
56
+ context_type: ContextTypeSchema.describe('One of 11 canonical types'),
57
+ temporal_class: TemporalClassSchema.optional().describe('Persistence duration - defaults by context_type'),
58
+ scope: ScopeSchema.optional().describe('global for personal/philosophy, project for technical'),
59
+
60
+ // Retrieval optimization (the secret sauce)
61
+ trigger_phrases: z.array(z.string()).describe('Situational patterns: "when debugging X", "working on Y"'),
62
+ semantic_tags: z.array(z.string()).describe('User-typeable concepts - avoid generic terms'),
63
+
64
+ // Optional categorization
65
+ domain: z.string().optional().describe('Specific area: embeddings, auth, family'),
66
+ feature: z.string().optional().describe('Specific feature within domain'),
67
+ related_files: z.array(z.string()).optional().describe('Source files for technical memories'),
68
+
69
+ // Flags
70
+ action_required: z.boolean().default(false).describe('Needs follow-up action'),
71
+ problem_solution_pair: z.boolean().default(false).describe('Problem→solution pattern'),
72
+ awaiting_implementation: z.boolean().optional().describe('Planned feature not yet built'),
73
+ awaiting_decision: z.boolean().optional().describe('Decision point needing resolution'),
74
+ })
75
+
76
+ /**
77
+ * Project snapshot - current state
78
+ */
79
+ export const ProjectSnapshotSchema = z.object({
80
+ current_phase: z.string().describe('What phase is the project in'),
81
+ recent_achievements: z.array(z.string()).describe('What was accomplished this session'),
82
+ active_challenges: z.array(z.string()).describe('Current blockers or challenges'),
83
+ next_steps: z.array(z.string()).describe('Planned next steps'),
84
+ })
85
+
86
+ /**
87
+ * Full curation result - what the curator returns
88
+ */
89
+ export const CurationResultSchema = z.object({
90
+ session_summary: z.string().describe('2-3 sentence overview of what happened'),
91
+ interaction_tone: z.string().nullable().optional().describe('How was the interaction'),
92
+ project_snapshot: ProjectSnapshotSchema.optional().describe('Current project state'),
93
+ memories: z.array(CuratedMemorySchema).describe('Extracted memories from session'),
94
+ })
95
+
96
+ // Type exports for TypeScript inference
97
+ export type ZodCuratedMemory = z.infer<typeof CuratedMemorySchema>
98
+ export type ZodCurationResult = z.infer<typeof CurationResultSchema>
99
+ export type ZodProjectSnapshot = z.infer<typeof ProjectSnapshotSchema>
100
+
101
+ /**
102
+ * Generate JSON Schema for SDK structured outputs
103
+ * Use: z.toJSONSchema(CurationResultSchema)
104
+ */
105
+ export function getCurationJsonSchema() {
106
+ return z.toJSONSchema(CurationResultSchema)
107
+ }