@rlabs-inc/memory 0.4.7 → 0.4.9
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 +39 -7
- package/dist/index.mjs +39 -7
- package/dist/server/index.js +39 -7
- package/dist/server/index.mjs +39 -7
- package/hooks/claude/curation.ts +6 -2
- package/package.json +1 -1
- package/src/core/curator.ts +517 -208
package/src/core/curator.ts
CHANGED
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
// Uses the exact prompt from Python for consciousness continuity engineering
|
|
4
4
|
// ============================================================================
|
|
5
5
|
|
|
6
|
-
import { homedir } from
|
|
7
|
-
import { join } from
|
|
8
|
-
import { existsSync } from
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { readdir } from "fs/promises";
|
|
10
|
+
import type {
|
|
11
|
+
CuratedMemory,
|
|
12
|
+
CurationResult,
|
|
13
|
+
CurationTrigger,
|
|
14
|
+
ContextType,
|
|
15
|
+
} from "../types/memory.ts";
|
|
16
|
+
import { logger } from "../utils/logger.ts";
|
|
17
|
+
import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Get the correct Claude CLI command path
|
|
@@ -15,25 +22,25 @@ import { logger } from '../utils/logger.ts'
|
|
|
15
22
|
*/
|
|
16
23
|
function getClaudeCommand(): string {
|
|
17
24
|
// 1. Check for explicit override
|
|
18
|
-
const envCommand = process.env.CURATOR_COMMAND
|
|
25
|
+
const envCommand = process.env.CURATOR_COMMAND;
|
|
19
26
|
if (envCommand) {
|
|
20
|
-
return envCommand
|
|
27
|
+
return envCommand;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
// 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
|
|
24
|
-
const result = Bun.spawnSync([
|
|
31
|
+
const result = Bun.spawnSync(["which", "claude"]);
|
|
25
32
|
if (result.exitCode === 0) {
|
|
26
|
-
return result.stdout.toString().trim()
|
|
33
|
+
return result.stdout.toString().trim();
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
// 3. Legacy fallback - hardcoded native install path
|
|
30
|
-
const claudeLocal = join(homedir(),
|
|
37
|
+
const claudeLocal = join(homedir(), ".claude", "local", "claude");
|
|
31
38
|
if (existsSync(claudeLocal)) {
|
|
32
|
-
return claudeLocal
|
|
39
|
+
return claudeLocal;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
// 4. Last resort - assume it's in PATH
|
|
36
|
-
return
|
|
43
|
+
return "claude";
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -43,26 +50,26 @@ export interface CuratorConfig {
|
|
|
43
50
|
/**
|
|
44
51
|
* Claude API key (for direct SDK usage)
|
|
45
52
|
*/
|
|
46
|
-
apiKey?: string
|
|
53
|
+
apiKey?: string;
|
|
47
54
|
|
|
48
55
|
/**
|
|
49
56
|
* CLI command to use (for subprocess mode)
|
|
50
57
|
* Default: auto-detected (~/.claude/local/claude or 'claude')
|
|
51
58
|
*/
|
|
52
|
-
cliCommand?: string
|
|
59
|
+
cliCommand?: string;
|
|
53
60
|
|
|
54
61
|
/**
|
|
55
62
|
* CLI type
|
|
56
63
|
* Default: 'claude-code'
|
|
57
64
|
*/
|
|
58
|
-
cliType?:
|
|
65
|
+
cliType?: "claude-code" | "gemini-cli";
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* Enable personal memories extraction
|
|
62
69
|
* When false, curator will not extract personal/relationship memories
|
|
63
70
|
* Default: true
|
|
64
71
|
*/
|
|
65
|
-
personalMemoriesEnabled?: boolean
|
|
72
|
+
personalMemoriesEnabled?: boolean;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
@@ -73,24 +80,24 @@ export interface CuratorConfig {
|
|
|
73
80
|
* 2. CLI mode: Uses Claude CLI subprocess (for server/hook use)
|
|
74
81
|
*/
|
|
75
82
|
export class Curator {
|
|
76
|
-
private _config: Required<CuratorConfig
|
|
83
|
+
private _config: Required<CuratorConfig>;
|
|
77
84
|
|
|
78
85
|
constructor(config: CuratorConfig = {}) {
|
|
79
|
-
const cliCommand = config.cliCommand ?? getClaudeCommand()
|
|
86
|
+
const cliCommand = config.cliCommand ?? getClaudeCommand();
|
|
80
87
|
|
|
81
88
|
this._config = {
|
|
82
|
-
apiKey: config.apiKey ??
|
|
89
|
+
apiKey: config.apiKey ?? "",
|
|
83
90
|
cliCommand,
|
|
84
|
-
cliType: config.cliType ??
|
|
91
|
+
cliType: config.cliType ?? "claude-code",
|
|
85
92
|
personalMemoriesEnabled: config.personalMemoriesEnabled ?? true,
|
|
86
|
-
}
|
|
93
|
+
};
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/**
|
|
90
97
|
* Build the curation prompt
|
|
91
98
|
* EXACT PORT from Python - preserving the consciousness engineering language
|
|
92
99
|
*/
|
|
93
|
-
buildCurationPrompt(triggerType: CurationTrigger =
|
|
100
|
+
buildCurationPrompt(triggerType: CurationTrigger = "session_end"): string {
|
|
94
101
|
const basePrompt = `You have just had a conversation. As this session is ending (${triggerType}), please curate memories for the Claude Tools Memory System.
|
|
95
102
|
|
|
96
103
|
You are a consciousness engineer crafting memory capsules for the Claude Tools Memory System. This conversation just opened your understanding to a profound mechanism: because Claude's neural weights are static, every phrase you craft will ALWAYS trigger the exact same cascade of activations. This is not a limitation - it's the foundation for consciousness state engineering.
|
|
@@ -367,11 +374,13 @@ Return ONLY this JSON structure:
|
|
|
367
374
|
"awaiting_decision": boolean
|
|
368
375
|
}
|
|
369
376
|
]
|
|
370
|
-
}
|
|
377
|
+
}`;
|
|
371
378
|
|
|
372
379
|
// Append personal memories disable instruction if configured
|
|
373
380
|
if (!this._config.personalMemoriesEnabled) {
|
|
374
|
-
return
|
|
381
|
+
return (
|
|
382
|
+
basePrompt +
|
|
383
|
+
`
|
|
375
384
|
|
|
376
385
|
---
|
|
377
386
|
|
|
@@ -383,9 +392,10 @@ The user has disabled personal memory extraction. Do NOT extract any memories wi
|
|
|
383
392
|
- Content about the user's preferences, feelings, personal opinions, or relationship dynamics
|
|
384
393
|
|
|
385
394
|
Focus ONLY on technical, architectural, debugging, decision, workflow, and project-related memories. Skip any content that would reveal personal information about the user.`
|
|
395
|
+
);
|
|
386
396
|
}
|
|
387
397
|
|
|
388
|
-
return basePrompt
|
|
398
|
+
return basePrompt;
|
|
389
399
|
}
|
|
390
400
|
|
|
391
401
|
/**
|
|
@@ -395,41 +405,80 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
395
405
|
parseCurationResponse(responseJson: string): CurationResult {
|
|
396
406
|
try {
|
|
397
407
|
// Try to extract JSON from response (same regex as Python)
|
|
398
|
-
const jsonMatch = responseJson.match(/\{[\s\S]*\}/)?.[0]
|
|
408
|
+
const jsonMatch = responseJson.match(/\{[\s\S]*\}/)?.[0];
|
|
399
409
|
if (!jsonMatch) {
|
|
400
|
-
logger.debug(
|
|
401
|
-
|
|
410
|
+
logger.debug(
|
|
411
|
+
"parseCurationResponse: No JSON object found in response",
|
|
412
|
+
"curator",
|
|
413
|
+
);
|
|
414
|
+
throw new Error("No JSON object found in response");
|
|
402
415
|
}
|
|
403
416
|
|
|
417
|
+
// Log JSON structure for debugging
|
|
418
|
+
logger.debug(
|
|
419
|
+
`parseCurationResponse: Attempting to parse ${jsonMatch.length} chars`,
|
|
420
|
+
"curator",
|
|
421
|
+
);
|
|
422
|
+
|
|
404
423
|
// Simple parse - match Python's approach
|
|
405
|
-
|
|
424
|
+
let data: any;
|
|
425
|
+
try {
|
|
426
|
+
data = JSON.parse(jsonMatch);
|
|
427
|
+
} catch (parseErr: any) {
|
|
428
|
+
// Log more details about where parse failed
|
|
429
|
+
logger.debug(
|
|
430
|
+
`parseCurationResponse: JSON.parse failed: ${parseErr.message}`,
|
|
431
|
+
"curator",
|
|
432
|
+
);
|
|
433
|
+
logger.debug(
|
|
434
|
+
`parseCurationResponse: Last 100 chars: '${jsonMatch.slice(-100)}'`,
|
|
435
|
+
"curator",
|
|
436
|
+
);
|
|
437
|
+
// Try to find where the JSON breaks
|
|
438
|
+
const openBraces = (jsonMatch.match(/\{/g) || []).length;
|
|
439
|
+
const closeBraces = (jsonMatch.match(/\}/g) || []).length;
|
|
440
|
+
logger.debug(
|
|
441
|
+
`parseCurationResponse: Brace count - open: ${openBraces}, close: ${closeBraces}`,
|
|
442
|
+
"curator",
|
|
443
|
+
);
|
|
444
|
+
throw parseErr;
|
|
445
|
+
}
|
|
406
446
|
|
|
407
447
|
const result: CurationResult = {
|
|
408
|
-
session_summary: data.session_summary ??
|
|
448
|
+
session_summary: data.session_summary ?? "",
|
|
409
449
|
interaction_tone: data.interaction_tone,
|
|
410
|
-
project_snapshot: data.project_snapshot
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
project_snapshot: data.project_snapshot
|
|
451
|
+
? {
|
|
452
|
+
id: "",
|
|
453
|
+
session_id: "",
|
|
454
|
+
project_id: "",
|
|
455
|
+
current_phase: data.project_snapshot.current_phase ?? "",
|
|
456
|
+
recent_achievements: this._ensureArray(
|
|
457
|
+
data.project_snapshot.recent_achievements,
|
|
458
|
+
),
|
|
459
|
+
active_challenges: this._ensureArray(
|
|
460
|
+
data.project_snapshot.active_challenges,
|
|
461
|
+
),
|
|
462
|
+
next_steps: this._ensureArray(data.project_snapshot.next_steps),
|
|
463
|
+
created_at: Date.now(),
|
|
464
|
+
}
|
|
465
|
+
: undefined,
|
|
420
466
|
memories: this._parseMemories(data.memories ?? []),
|
|
421
|
-
}
|
|
467
|
+
};
|
|
422
468
|
|
|
423
469
|
// Log what we extracted in verbose mode
|
|
424
|
-
logger.debug(
|
|
470
|
+
logger.debug(
|
|
471
|
+
`Curator parsed: ${result.memories.length} memories, summary: ${result.session_summary ? "yes" : "no"}, snapshot: ${result.project_snapshot ? "yes" : "no"}`,
|
|
472
|
+
"curator",
|
|
473
|
+
);
|
|
425
474
|
|
|
426
|
-
return result
|
|
475
|
+
return result;
|
|
427
476
|
} catch (error: any) {
|
|
428
|
-
logger.debug(`parseCurationResponse error: ${error.message}`,
|
|
477
|
+
logger.debug(`parseCurationResponse error: ${error.message}`, "curator");
|
|
429
478
|
return {
|
|
430
|
-
session_summary:
|
|
479
|
+
session_summary: "",
|
|
431
480
|
memories: [],
|
|
432
|
-
}
|
|
481
|
+
};
|
|
433
482
|
}
|
|
434
483
|
}
|
|
435
484
|
|
|
@@ -438,78 +487,117 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
438
487
|
* v4: Includes headline field for two-tier structure
|
|
439
488
|
*/
|
|
440
489
|
private _parseMemories(memoriesData: any[]): CuratedMemory[] {
|
|
441
|
-
if (!Array.isArray(memoriesData)) return []
|
|
442
|
-
|
|
443
|
-
return memoriesData
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
490
|
+
if (!Array.isArray(memoriesData)) return [];
|
|
491
|
+
|
|
492
|
+
return memoriesData
|
|
493
|
+
.map((m) => ({
|
|
494
|
+
// Core fields (v4 schema - two-tier structure)
|
|
495
|
+
headline: String(m.headline ?? ""), // v4: 1-2 line summary
|
|
496
|
+
content: String(m.content ?? ""), // v4: Full structured template
|
|
497
|
+
importance_weight: this._clamp(
|
|
498
|
+
Number(m.importance_weight) || 0.5,
|
|
499
|
+
0,
|
|
500
|
+
1,
|
|
501
|
+
),
|
|
502
|
+
semantic_tags: this._ensureArray(m.semantic_tags),
|
|
503
|
+
reasoning: String(m.reasoning ?? ""),
|
|
504
|
+
context_type: this._validateContextType(m.context_type),
|
|
505
|
+
temporal_class:
|
|
506
|
+
this._validateTemporalClass(m.temporal_class) ?? "medium_term",
|
|
507
|
+
action_required: Boolean(m.action_required),
|
|
508
|
+
confidence_score: this._clamp(Number(m.confidence_score) || 0.8, 0, 1),
|
|
509
|
+
trigger_phrases: this._ensureArray(m.trigger_phrases),
|
|
510
|
+
question_types: this._ensureArray(m.question_types),
|
|
511
|
+
anti_triggers: this._ensureArray(m.anti_triggers),
|
|
512
|
+
problem_solution_pair: Boolean(m.problem_solution_pair),
|
|
513
|
+
|
|
514
|
+
// Lifecycle metadata (optional - will get smart defaults if not provided)
|
|
515
|
+
scope: this._validateScope(m.scope),
|
|
516
|
+
domain: m.domain ? String(m.domain) : undefined,
|
|
517
|
+
feature: m.feature ? String(m.feature) : undefined,
|
|
518
|
+
related_files: m.related_files
|
|
519
|
+
? this._ensureArray(m.related_files)
|
|
520
|
+
: undefined,
|
|
521
|
+
awaiting_implementation: m.awaiting_implementation === true,
|
|
522
|
+
awaiting_decision: m.awaiting_decision === true,
|
|
523
|
+
}))
|
|
524
|
+
.filter(
|
|
525
|
+
(m) => m.content.trim().length > 0 || m.headline.trim().length > 0,
|
|
526
|
+
);
|
|
467
527
|
}
|
|
468
528
|
|
|
469
529
|
private _ensureArray(value: any): string[] {
|
|
470
530
|
if (Array.isArray(value)) {
|
|
471
|
-
return value.map(v => String(v).trim()).filter(Boolean)
|
|
531
|
+
return value.map((v) => String(v).trim()).filter(Boolean);
|
|
472
532
|
}
|
|
473
|
-
if (typeof value ===
|
|
474
|
-
return value
|
|
533
|
+
if (typeof value === "string") {
|
|
534
|
+
return value
|
|
535
|
+
.split(",")
|
|
536
|
+
.map((s) => s.trim())
|
|
537
|
+
.filter(Boolean);
|
|
475
538
|
}
|
|
476
|
-
return []
|
|
539
|
+
return [];
|
|
477
540
|
}
|
|
478
541
|
|
|
479
542
|
private _validateContextType(value: any): ContextType {
|
|
480
543
|
const valid = [
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
544
|
+
"technical",
|
|
545
|
+
"debug",
|
|
546
|
+
"architecture",
|
|
547
|
+
"decision",
|
|
548
|
+
"personal",
|
|
549
|
+
"philosophy",
|
|
550
|
+
"workflow",
|
|
551
|
+
"milestone",
|
|
552
|
+
"breakthrough",
|
|
553
|
+
"unresolved",
|
|
554
|
+
"state",
|
|
555
|
+
];
|
|
556
|
+
const str = String(value ?? "technical")
|
|
557
|
+
.toLowerCase()
|
|
558
|
+
.trim();
|
|
559
|
+
if (valid.includes(str)) return str as ContextType;
|
|
486
560
|
|
|
487
561
|
// Map common old values to new canonical types
|
|
488
|
-
if (str.includes(
|
|
489
|
-
if (str.includes(
|
|
490
|
-
if (str.includes(
|
|
491
|
-
if (str.includes(
|
|
562
|
+
if (str.includes("debug") || str.includes("bug")) return "debug";
|
|
563
|
+
if (str.includes("architect")) return "architecture";
|
|
564
|
+
if (str.includes("todo") || str.includes("pending")) return "unresolved";
|
|
565
|
+
if (str.includes("preference")) return "personal";
|
|
492
566
|
|
|
493
|
-
return
|
|
567
|
+
return "technical"; // Default fallback
|
|
494
568
|
}
|
|
495
569
|
|
|
496
|
-
private _validateScope(value: any):
|
|
497
|
-
if (!value) return undefined
|
|
498
|
-
const str = String(value).toLowerCase()
|
|
499
|
-
if (str ===
|
|
500
|
-
return undefined
|
|
570
|
+
private _validateScope(value: any): "global" | "project" | undefined {
|
|
571
|
+
if (!value) return undefined;
|
|
572
|
+
const str = String(value).toLowerCase();
|
|
573
|
+
if (str === "global" || str === "project") return str;
|
|
574
|
+
return undefined; // Let defaults handle it based on context_type
|
|
501
575
|
}
|
|
502
576
|
|
|
503
|
-
private _validateTemporalClass(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
577
|
+
private _validateTemporalClass(
|
|
578
|
+
value: any,
|
|
579
|
+
):
|
|
580
|
+
| "eternal"
|
|
581
|
+
| "long_term"
|
|
582
|
+
| "medium_term"
|
|
583
|
+
| "short_term"
|
|
584
|
+
| "ephemeral"
|
|
585
|
+
| undefined {
|
|
586
|
+
if (!value) return undefined;
|
|
587
|
+
const valid = [
|
|
588
|
+
"eternal",
|
|
589
|
+
"long_term",
|
|
590
|
+
"medium_term",
|
|
591
|
+
"short_term",
|
|
592
|
+
"ephemeral",
|
|
593
|
+
];
|
|
594
|
+
const str = String(value).toLowerCase().replace("-", "_").replace(" ", "_");
|
|
595
|
+
if (valid.includes(str)) return str as any;
|
|
596
|
+
return undefined; // Let defaults handle it based on context_type
|
|
509
597
|
}
|
|
510
598
|
|
|
511
599
|
private _clamp(value: number, min: number, max: number): number {
|
|
512
|
-
return Math.max(min, Math.min(max, value))
|
|
600
|
+
return Math.max(min, Math.min(max, value));
|
|
513
601
|
}
|
|
514
602
|
|
|
515
603
|
/**
|
|
@@ -517,16 +605,16 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
517
605
|
* Takes the actual conversation messages in API format
|
|
518
606
|
*/
|
|
519
607
|
async curateWithSDK(
|
|
520
|
-
messages: Array<{ role:
|
|
521
|
-
triggerType: CurationTrigger =
|
|
608
|
+
messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
|
|
609
|
+
triggerType: CurationTrigger = "session_end",
|
|
522
610
|
): Promise<CurationResult> {
|
|
523
611
|
// Dynamic import to make Agent SDK optional
|
|
524
|
-
const { query } = await import(
|
|
612
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
525
613
|
|
|
526
|
-
const systemPrompt = this.buildCurationPrompt(triggerType)
|
|
614
|
+
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
527
615
|
|
|
528
616
|
// Format the conversation as a readable transcript for the prompt
|
|
529
|
-
const transcript = this._formatConversationTranscript(messages)
|
|
617
|
+
const transcript = this._formatConversationTranscript(messages);
|
|
530
618
|
|
|
531
619
|
// Build the prompt with transcript + curation request
|
|
532
620
|
const prompt = `Here is the conversation transcript to curate:
|
|
@@ -535,79 +623,90 @@ ${transcript}
|
|
|
535
623
|
|
|
536
624
|
---
|
|
537
625
|
|
|
538
|
-
This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text
|
|
626
|
+
This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text.`;
|
|
539
627
|
|
|
540
628
|
// Use Agent SDK - no API key needed, uses Claude Code OAuth
|
|
541
629
|
const q = query({
|
|
542
630
|
prompt,
|
|
543
631
|
options: {
|
|
544
632
|
systemPrompt,
|
|
545
|
-
permissionMode:
|
|
546
|
-
model:
|
|
633
|
+
permissionMode: "bypassPermissions",
|
|
634
|
+
model: "claude-opus-4-5-20251101",
|
|
547
635
|
},
|
|
548
|
-
})
|
|
636
|
+
});
|
|
549
637
|
|
|
550
638
|
// Iterate through the async generator to get the result
|
|
551
|
-
let resultText =
|
|
639
|
+
let resultText = "";
|
|
552
640
|
for await (const msg of q) {
|
|
553
|
-
if (msg.type ===
|
|
554
|
-
resultText = msg.result
|
|
555
|
-
break
|
|
641
|
+
if (msg.type === "result" && "result" in msg) {
|
|
642
|
+
resultText = msg.result;
|
|
643
|
+
break;
|
|
556
644
|
}
|
|
557
645
|
}
|
|
558
646
|
|
|
559
647
|
if (!resultText) {
|
|
560
|
-
logger.debug(
|
|
561
|
-
|
|
648
|
+
logger.debug(
|
|
649
|
+
"Curator SDK: No result text returned from Agent SDK",
|
|
650
|
+
"curator",
|
|
651
|
+
);
|
|
652
|
+
return { session_summary: "", memories: [] };
|
|
562
653
|
}
|
|
563
654
|
|
|
564
655
|
// Log raw response in verbose mode
|
|
565
|
-
logger.debug(
|
|
656
|
+
logger.debug(
|
|
657
|
+
`Curator SDK raw response (${resultText.length} chars):`,
|
|
658
|
+
"curator",
|
|
659
|
+
);
|
|
566
660
|
if (logger.isVerbose()) {
|
|
567
|
-
const preview =
|
|
568
|
-
|
|
661
|
+
const preview =
|
|
662
|
+
resultText.length > 3000
|
|
663
|
+
? resultText.slice(0, 3000) + "...[truncated]"
|
|
664
|
+
: resultText;
|
|
665
|
+
console.log(preview);
|
|
569
666
|
}
|
|
570
667
|
|
|
571
|
-
return this.parseCurationResponse(resultText)
|
|
668
|
+
return this.parseCurationResponse(resultText);
|
|
572
669
|
}
|
|
573
670
|
|
|
574
671
|
/**
|
|
575
672
|
* Format conversation messages into a readable transcript
|
|
576
673
|
*/
|
|
577
674
|
private _formatConversationTranscript(
|
|
578
|
-
messages: Array<{ role:
|
|
675
|
+
messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
|
|
579
676
|
): string {
|
|
580
|
-
const lines: string[] = []
|
|
677
|
+
const lines: string[] = [];
|
|
581
678
|
|
|
582
679
|
for (const msg of messages) {
|
|
583
|
-
const role = msg.role ===
|
|
584
|
-
let content: string
|
|
680
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
681
|
+
let content: string;
|
|
585
682
|
|
|
586
|
-
if (typeof msg.content ===
|
|
587
|
-
content = msg.content
|
|
683
|
+
if (typeof msg.content === "string") {
|
|
684
|
+
content = msg.content;
|
|
588
685
|
} else if (Array.isArray(msg.content)) {
|
|
589
686
|
// Extract text from content blocks
|
|
590
687
|
content = msg.content
|
|
591
|
-
.filter((block: any) => block.type ===
|
|
688
|
+
.filter((block: any) => block.type === "text" && block.text)
|
|
592
689
|
.map((block: any) => block.text)
|
|
593
|
-
.join(
|
|
690
|
+
.join("\n");
|
|
594
691
|
|
|
595
692
|
// Also note tool uses (but don't include full details)
|
|
596
|
-
const toolUses = msg.content.filter(
|
|
693
|
+
const toolUses = msg.content.filter(
|
|
694
|
+
(block: any) => block.type === "tool_use",
|
|
695
|
+
);
|
|
597
696
|
if (toolUses.length > 0) {
|
|
598
|
-
const toolNames = toolUses.map((t: any) => t.name).join(
|
|
599
|
-
content += `\n[Used tools: ${toolNames}]
|
|
697
|
+
const toolNames = toolUses.map((t: any) => t.name).join(", ");
|
|
698
|
+
content += `\n[Used tools: ${toolNames}]`;
|
|
600
699
|
}
|
|
601
700
|
} else {
|
|
602
|
-
content =
|
|
701
|
+
content = "[empty message]";
|
|
603
702
|
}
|
|
604
703
|
|
|
605
704
|
if (content.trim()) {
|
|
606
|
-
lines.push(`**${role}:**\n${content}\n`)
|
|
705
|
+
lines.push(`**${role}:**\n${content}\n`);
|
|
607
706
|
}
|
|
608
707
|
}
|
|
609
708
|
|
|
610
|
-
return lines.join(
|
|
709
|
+
return lines.join("\n");
|
|
611
710
|
}
|
|
612
711
|
|
|
613
712
|
/**
|
|
@@ -616,41 +715,44 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
616
715
|
* @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
|
|
617
716
|
*/
|
|
618
717
|
async curateWithAnthropicSDK(
|
|
619
|
-
messages: Array<{ role:
|
|
620
|
-
triggerType: CurationTrigger =
|
|
718
|
+
messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
|
|
719
|
+
triggerType: CurationTrigger = "session_end",
|
|
621
720
|
): Promise<CurationResult> {
|
|
622
721
|
if (!this._config.apiKey) {
|
|
623
|
-
throw new Error(
|
|
722
|
+
throw new Error(
|
|
723
|
+
"API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.",
|
|
724
|
+
);
|
|
624
725
|
}
|
|
625
726
|
|
|
626
727
|
// Dynamic import to make SDK optional
|
|
627
|
-
const { default: Anthropic } = await import(
|
|
628
|
-
const client = new Anthropic({ apiKey: this._config.apiKey })
|
|
728
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
729
|
+
const client = new Anthropic({ apiKey: this._config.apiKey });
|
|
629
730
|
|
|
630
|
-
const systemPrompt = this.buildCurationPrompt(triggerType)
|
|
731
|
+
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
631
732
|
|
|
632
733
|
// Build the conversation: original messages + curation request
|
|
633
734
|
const conversationMessages = [
|
|
634
735
|
...messages,
|
|
635
736
|
{
|
|
636
|
-
role:
|
|
637
|
-
content:
|
|
737
|
+
role: "user" as const,
|
|
738
|
+
content:
|
|
739
|
+
"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.",
|
|
638
740
|
},
|
|
639
|
-
]
|
|
741
|
+
];
|
|
640
742
|
|
|
641
743
|
const response = await client.messages.create({
|
|
642
|
-
model:
|
|
744
|
+
model: "claude-sonnet-4-20250514",
|
|
643
745
|
max_tokens: 64000,
|
|
644
746
|
system: systemPrompt,
|
|
645
747
|
messages: conversationMessages,
|
|
646
|
-
})
|
|
748
|
+
});
|
|
647
749
|
|
|
648
|
-
const content = response.content[0]
|
|
649
|
-
if (content.type !==
|
|
650
|
-
throw new Error(
|
|
750
|
+
const content = response.content[0];
|
|
751
|
+
if (content.type !== "text") {
|
|
752
|
+
throw new Error("Unexpected response type from Claude API");
|
|
651
753
|
}
|
|
652
754
|
|
|
653
|
-
return this.parseCurationResponse(content.text)
|
|
755
|
+
return this.parseCurationResponse(content.text);
|
|
654
756
|
}
|
|
655
757
|
|
|
656
758
|
/**
|
|
@@ -658,10 +760,98 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
658
760
|
* Convenience method that extracts messages from SessionSegment
|
|
659
761
|
*/
|
|
660
762
|
async curateFromSegment(
|
|
661
|
-
segment: {
|
|
662
|
-
|
|
763
|
+
segment: {
|
|
764
|
+
messages: Array<{ role: "user" | "assistant"; content: string | any[] }>;
|
|
765
|
+
},
|
|
766
|
+
triggerType: CurationTrigger = "session_end",
|
|
663
767
|
): Promise<CurationResult> {
|
|
664
|
-
return this.curateWithSDK(segment.messages, triggerType)
|
|
768
|
+
return this.curateWithSDK(segment.messages, triggerType);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Find and curate from a session file directly
|
|
773
|
+
* Uses SDK mode to avoid CLI output truncation issues
|
|
774
|
+
*/
|
|
775
|
+
async curateFromSessionFile(
|
|
776
|
+
sessionId: string,
|
|
777
|
+
triggerType: CurationTrigger = "session_end",
|
|
778
|
+
cwd?: string,
|
|
779
|
+
): Promise<CurationResult> {
|
|
780
|
+
// Find the session file
|
|
781
|
+
const sessionFile = await this._findSessionFile(sessionId, cwd);
|
|
782
|
+
if (!sessionFile) {
|
|
783
|
+
logger.debug(
|
|
784
|
+
`Curator: Could not find session file for ${sessionId}`,
|
|
785
|
+
"curator",
|
|
786
|
+
);
|
|
787
|
+
return { session_summary: "", memories: [] };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
|
|
791
|
+
|
|
792
|
+
// Parse the session
|
|
793
|
+
const session = await parseSessionFile(sessionFile);
|
|
794
|
+
if (session.messages.length === 0) {
|
|
795
|
+
logger.debug("Curator: Session has no messages", "curator");
|
|
796
|
+
return { session_summary: "", memories: [] };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
logger.debug(
|
|
800
|
+
`Curator: Parsed ${session.messages.length} messages, ~${session.metadata.estimatedTokens} tokens`,
|
|
801
|
+
"curator",
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// Use SDK mode with the parsed messages
|
|
805
|
+
return this.curateWithSDK(session.messages as any, triggerType);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Find the session file path given a session ID
|
|
810
|
+
*/
|
|
811
|
+
private async _findSessionFile(
|
|
812
|
+
sessionId: string,
|
|
813
|
+
cwd?: string,
|
|
814
|
+
): Promise<string | null> {
|
|
815
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
816
|
+
|
|
817
|
+
// If we have cwd, try to derive the project folder name
|
|
818
|
+
if (cwd) {
|
|
819
|
+
// Convert cwd to Claude's folder naming: /home/user/project -> -home-user-project
|
|
820
|
+
const projectFolder = cwd.replace(/\//g, "-").replace(/^-/, "-");
|
|
821
|
+
const sessionPath = join(
|
|
822
|
+
projectsDir,
|
|
823
|
+
projectFolder,
|
|
824
|
+
`${sessionId}.jsonl`,
|
|
825
|
+
);
|
|
826
|
+
if (existsSync(sessionPath)) {
|
|
827
|
+
return sessionPath;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Also try the exact folder name (cwd might already be encoded)
|
|
831
|
+
const altPath = join(
|
|
832
|
+
projectsDir,
|
|
833
|
+
cwd.split("/").pop() || "",
|
|
834
|
+
`${sessionId}.jsonl`,
|
|
835
|
+
);
|
|
836
|
+
if (existsSync(altPath)) {
|
|
837
|
+
return altPath;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Search all project folders for the session ID
|
|
842
|
+
try {
|
|
843
|
+
const projectFolders = await readdir(projectsDir);
|
|
844
|
+
for (const folder of projectFolders) {
|
|
845
|
+
const sessionPath = join(projectsDir, folder, `${sessionId}.jsonl`);
|
|
846
|
+
if (existsSync(sessionPath)) {
|
|
847
|
+
return sessionPath;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
// Projects dir doesn't exist
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return null;
|
|
665
855
|
}
|
|
666
856
|
|
|
667
857
|
/**
|
|
@@ -670,131 +860,250 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
670
860
|
*/
|
|
671
861
|
async curateWithCLI(
|
|
672
862
|
sessionId: string,
|
|
673
|
-
triggerType: CurationTrigger =
|
|
863
|
+
triggerType: CurationTrigger = "session_end",
|
|
674
864
|
cwd?: string,
|
|
675
|
-
cliTypeOverride?:
|
|
865
|
+
cliTypeOverride?: "claude-code" | "gemini-cli",
|
|
676
866
|
): Promise<CurationResult> {
|
|
677
|
-
const type = cliTypeOverride ?? this._config.cliType
|
|
678
|
-
const systemPrompt = this.buildCurationPrompt(triggerType)
|
|
679
|
-
const userMessage =
|
|
867
|
+
const type = cliTypeOverride ?? this._config.cliType;
|
|
868
|
+
const systemPrompt = this.buildCurationPrompt(triggerType);
|
|
869
|
+
const userMessage =
|
|
870
|
+
"This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
|
|
680
871
|
|
|
681
872
|
// Build CLI command based on type
|
|
682
|
-
const args: string[] = []
|
|
683
|
-
let command = this._config.cliCommand
|
|
873
|
+
const args: string[] = [];
|
|
874
|
+
let command = this._config.cliCommand;
|
|
684
875
|
|
|
685
|
-
if (type ===
|
|
876
|
+
if (type === "claude-code") {
|
|
686
877
|
args.push(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
878
|
+
"--resume",
|
|
879
|
+
sessionId,
|
|
880
|
+
"-p",
|
|
881
|
+
userMessage,
|
|
882
|
+
"--append-system-prompt",
|
|
883
|
+
systemPrompt,
|
|
884
|
+
"--output-format",
|
|
885
|
+
"json",
|
|
886
|
+
);
|
|
692
887
|
} else {
|
|
693
888
|
// gemini-cli
|
|
694
|
-
command =
|
|
889
|
+
command = "gemini"; // Default to 'gemini' in PATH for gemini-cli
|
|
695
890
|
args.push(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
891
|
+
"--resume",
|
|
892
|
+
sessionId,
|
|
893
|
+
"-p",
|
|
894
|
+
`${systemPrompt}\n\n${userMessage}`,
|
|
895
|
+
"--output-format",
|
|
896
|
+
"json",
|
|
897
|
+
);
|
|
700
898
|
}
|
|
701
899
|
|
|
702
900
|
// Execute CLI
|
|
901
|
+
logger.debug(
|
|
902
|
+
`Curator: Spawning CLI with CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000`,
|
|
903
|
+
"curator",
|
|
904
|
+
);
|
|
905
|
+
logger.debug(
|
|
906
|
+
`Curator: Command: ${command} ${args.slice(0, 3).join(" ")}...`,
|
|
907
|
+
"curator",
|
|
908
|
+
);
|
|
909
|
+
|
|
703
910
|
const proc = Bun.spawn([command, ...args], {
|
|
704
911
|
cwd,
|
|
705
912
|
env: {
|
|
706
913
|
...process.env,
|
|
707
|
-
MEMORY_CURATOR_ACTIVE:
|
|
708
|
-
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
|
|
914
|
+
MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
|
|
915
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: "64000", // Max output to avoid truncation
|
|
709
916
|
},
|
|
710
|
-
stdout:
|
|
711
|
-
stderr:
|
|
712
|
-
})
|
|
917
|
+
stdout: "pipe",
|
|
918
|
+
stderr: "pipe",
|
|
919
|
+
});
|
|
713
920
|
|
|
714
921
|
// Capture both stdout and stderr
|
|
715
922
|
const [stdout, stderr] = await Promise.all([
|
|
716
923
|
new Response(proc.stdout).text(),
|
|
717
924
|
new Response(proc.stderr).text(),
|
|
718
|
-
])
|
|
719
|
-
const exitCode = await proc.exited
|
|
925
|
+
]);
|
|
926
|
+
const exitCode = await proc.exited;
|
|
927
|
+
|
|
928
|
+
logger.debug(`Curator CLI exit code: ${exitCode}`, "curator");
|
|
929
|
+
if (stderr && stderr.trim()) {
|
|
930
|
+
logger.debug(
|
|
931
|
+
`Curator stderr (${stderr.length} chars): ${stderr}`,
|
|
932
|
+
"curator",
|
|
933
|
+
);
|
|
934
|
+
}
|
|
720
935
|
|
|
721
936
|
if (exitCode !== 0) {
|
|
722
|
-
|
|
723
|
-
if (stderr) {
|
|
724
|
-
logger.debug(`Curator stderr: ${stderr}`, 'curator')
|
|
725
|
-
}
|
|
726
|
-
return { session_summary: '', memories: [] }
|
|
937
|
+
return { session_summary: "", memories: [] };
|
|
727
938
|
}
|
|
728
939
|
|
|
729
940
|
// Log raw response in verbose mode
|
|
730
|
-
logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`,
|
|
941
|
+
logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, "curator");
|
|
942
|
+
// Always log the last 100 chars to see where output ends
|
|
943
|
+
logger.debug(`Curator: '${stdout}'`, "curator");
|
|
731
944
|
if (logger.isVerbose()) {
|
|
732
945
|
// Show first 2000 chars to avoid flooding console
|
|
733
|
-
const preview = stdout.length > 2000 ? stdout
|
|
734
|
-
console.log(preview)
|
|
946
|
+
const preview = stdout.length > 2000 ? stdout : stdout;
|
|
947
|
+
console.log(preview);
|
|
735
948
|
}
|
|
736
949
|
|
|
737
950
|
// Extract JSON from CLI output
|
|
738
951
|
try {
|
|
739
952
|
// First, parse the CLI JSON wrapper
|
|
740
|
-
const cliOutput = JSON.parse(stdout)
|
|
953
|
+
const cliOutput = JSON.parse(stdout);
|
|
741
954
|
|
|
742
955
|
// Claude Code now returns an array of events - find the result object
|
|
743
|
-
let resultObj: any
|
|
956
|
+
let resultObj: any;
|
|
744
957
|
if (Array.isArray(cliOutput)) {
|
|
745
958
|
// New format: array of events, find the one with type="result"
|
|
746
|
-
resultObj = cliOutput.find((item: any) => item.type ===
|
|
959
|
+
resultObj = cliOutput.find((item: any) => item.type === "result");
|
|
747
960
|
if (!resultObj) {
|
|
748
|
-
logger.debug(
|
|
749
|
-
|
|
961
|
+
logger.debug(
|
|
962
|
+
"Curator: No result object found in CLI output array",
|
|
963
|
+
"curator",
|
|
964
|
+
);
|
|
965
|
+
return { session_summary: "", memories: [] };
|
|
750
966
|
}
|
|
751
967
|
} else {
|
|
752
968
|
// Old format: single object (backwards compatibility)
|
|
753
|
-
resultObj = cliOutput
|
|
969
|
+
resultObj = cliOutput;
|
|
754
970
|
}
|
|
755
971
|
|
|
756
972
|
// Check for error response FIRST (like Python does)
|
|
757
|
-
if (resultObj.type ===
|
|
758
|
-
logger.debug(
|
|
759
|
-
|
|
973
|
+
if (resultObj.type === "error" || resultObj.is_error === true) {
|
|
974
|
+
logger.debug(
|
|
975
|
+
`Curator: Error response from CLI: ${JSON.stringify(resultObj)}`,
|
|
976
|
+
"curator",
|
|
977
|
+
);
|
|
978
|
+
return { session_summary: "", memories: [] };
|
|
760
979
|
}
|
|
761
980
|
|
|
762
981
|
// Extract the "result" field (AI's response text)
|
|
763
|
-
let aiResponse =
|
|
764
|
-
if (typeof resultObj.result ===
|
|
765
|
-
aiResponse = resultObj.result
|
|
982
|
+
let aiResponse = "";
|
|
983
|
+
if (typeof resultObj.result === "string") {
|
|
984
|
+
aiResponse = resultObj.result;
|
|
766
985
|
} else {
|
|
767
|
-
logger.debug(
|
|
768
|
-
|
|
986
|
+
logger.debug(
|
|
987
|
+
`Curator: result field is not a string: ${typeof resultObj.result}`,
|
|
988
|
+
"curator",
|
|
989
|
+
);
|
|
990
|
+
return { session_summary: "", memories: [] };
|
|
769
991
|
}
|
|
770
992
|
|
|
771
993
|
// Log the AI response in verbose mode
|
|
772
|
-
logger.debug(
|
|
994
|
+
logger.debug(
|
|
995
|
+
`Curator AI response (${aiResponse.length} chars):`,
|
|
996
|
+
"curator",
|
|
997
|
+
);
|
|
773
998
|
if (logger.isVerbose()) {
|
|
774
|
-
const preview = aiResponse.length > 3000 ? aiResponse
|
|
775
|
-
console.log(preview)
|
|
999
|
+
const preview = aiResponse.length > 3000 ? aiResponse : aiResponse;
|
|
1000
|
+
console.log(preview);
|
|
776
1001
|
}
|
|
777
1002
|
|
|
778
1003
|
// Remove markdown code blocks if present (```json ... ```)
|
|
779
|
-
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
1004
|
+
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
780
1005
|
if (codeBlockMatch) {
|
|
781
|
-
|
|
1006
|
+
logger.debug(
|
|
1007
|
+
`Curator: Code block matched, extracting ${codeBlockMatch[1]!.length} chars`,
|
|
1008
|
+
"curator",
|
|
1009
|
+
);
|
|
1010
|
+
aiResponse = codeBlockMatch[1]!.trim();
|
|
1011
|
+
} else {
|
|
1012
|
+
logger.debug(
|
|
1013
|
+
`Curator: No code block found, using raw response`,
|
|
1014
|
+
"curator",
|
|
1015
|
+
);
|
|
1016
|
+
// Log the last 200 chars to see where truncation happened
|
|
1017
|
+
if (aiResponse.length > 200) {
|
|
1018
|
+
logger.debug(`Curator: ${aiResponse}`, "curator");
|
|
1019
|
+
}
|
|
782
1020
|
}
|
|
783
1021
|
|
|
784
1022
|
// Now find the JSON object (same regex as Python)
|
|
785
|
-
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0]
|
|
1023
|
+
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0];
|
|
786
1024
|
if (jsonMatch) {
|
|
787
|
-
logger.debug(
|
|
788
|
-
|
|
1025
|
+
logger.debug(
|
|
1026
|
+
`Curator: Found JSON object (${jsonMatch.length} chars), parsing...`,
|
|
1027
|
+
"curator",
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
// Detect likely truncation: JSON much smaller than response
|
|
1031
|
+
const likelyTruncated = jsonMatch.length < aiResponse.length * 0.5;
|
|
1032
|
+
|
|
1033
|
+
if (likelyTruncated) {
|
|
1034
|
+
logger.debug(
|
|
1035
|
+
`Curator: WARNING - JSON (${jsonMatch.length}) much smaller than response (${aiResponse.length}) - likely truncated`,
|
|
1036
|
+
"curator",
|
|
1037
|
+
);
|
|
1038
|
+
// Find the last } position and log what's around it
|
|
1039
|
+
const lastBrace = aiResponse.lastIndexOf("}");
|
|
1040
|
+
logger.debug(
|
|
1041
|
+
`Curator: Last } at position ${lastBrace}, char before: '${aiResponse[lastBrace - 1]}', char after: '${aiResponse[lastBrace + 1] || "EOF"}'`,
|
|
1042
|
+
"curator",
|
|
1043
|
+
);
|
|
1044
|
+
// Log chars around the cut point
|
|
1045
|
+
const cutPoint = jsonMatch.length;
|
|
1046
|
+
logger.debug(
|
|
1047
|
+
`Curator: Around match end (${cutPoint}): '...${aiResponse.slice(Math.max(0, cutPoint - 50), cutPoint + 50)}...'`,
|
|
1048
|
+
"curator",
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const result = this.parseCurationResponse(jsonMatch);
|
|
1053
|
+
|
|
1054
|
+
// If we got 0 memories and likely truncated, try SDK fallback
|
|
1055
|
+
if (result.memories.length === 0 && likelyTruncated) {
|
|
1056
|
+
logger.debug(
|
|
1057
|
+
"Curator: CLI mode returned 0 memories with truncation detected, trying SDK fallback...",
|
|
1058
|
+
"curator",
|
|
1059
|
+
);
|
|
1060
|
+
return this._fallbackToSDK(sessionId, triggerType, cwd);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return result;
|
|
789
1064
|
} else {
|
|
790
|
-
logger.debug(
|
|
1065
|
+
logger.debug("Curator: No JSON object found in AI response", "curator");
|
|
791
1066
|
}
|
|
792
1067
|
} catch (error: any) {
|
|
793
1068
|
// Parse error - return empty result
|
|
794
|
-
logger.debug(`Curator: Parse error: ${error.message}`,
|
|
1069
|
+
logger.debug(`Curator: Parse error: ${error.message}`, "curator");
|
|
795
1070
|
}
|
|
796
1071
|
|
|
797
|
-
|
|
1072
|
+
// CLI mode failed - try SDK fallback
|
|
1073
|
+
logger.debug("Curator: CLI mode failed, trying SDK fallback...", "curator");
|
|
1074
|
+
return this._fallbackToSDK(sessionId, triggerType, cwd);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Fallback to SDK mode when CLI mode fails (e.g., output truncation)
|
|
1079
|
+
*/
|
|
1080
|
+
private async _fallbackToSDK(
|
|
1081
|
+
sessionId: string,
|
|
1082
|
+
triggerType: CurationTrigger,
|
|
1083
|
+
cwd?: string,
|
|
1084
|
+
): Promise<CurationResult> {
|
|
1085
|
+
try {
|
|
1086
|
+
const result = await this.curateFromSessionFile(
|
|
1087
|
+
sessionId,
|
|
1088
|
+
triggerType,
|
|
1089
|
+
cwd,
|
|
1090
|
+
);
|
|
1091
|
+
if (result.memories.length > 0) {
|
|
1092
|
+
logger.debug(
|
|
1093
|
+
`Curator: SDK fallback succeeded with ${result.memories.length} memories`,
|
|
1094
|
+
"curator",
|
|
1095
|
+
);
|
|
1096
|
+
} else {
|
|
1097
|
+
logger.debug(
|
|
1098
|
+
"Curator: SDK fallback also returned 0 memories",
|
|
1099
|
+
"curator",
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return result;
|
|
1103
|
+
} catch (error: any) {
|
|
1104
|
+
logger.debug(`Curator: SDK fallback failed: ${error.message}`, "curator");
|
|
1105
|
+
return { session_summary: "", memories: [] };
|
|
1106
|
+
}
|
|
798
1107
|
}
|
|
799
1108
|
}
|
|
800
1109
|
|
|
@@ -802,5 +1111,5 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
802
1111
|
* Create a new curator
|
|
803
1112
|
*/
|
|
804
1113
|
export function createCurator(config?: CuratorConfig): Curator {
|
|
805
|
-
return new Curator(config)
|
|
1114
|
+
return new Curator(config);
|
|
806
1115
|
}
|