a2a-memory 0.9.0 → 0.10.0
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/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +1 -0
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/claude-sync.d.ts.map +1 -1
- package/dist/cli/commands/claude-sync.js +1 -0
- package/dist/cli/commands/claude-sync.js.map +1 -1
- package/dist/cli/commands/cleanup.d.ts.map +1 -1
- package/dist/cli/commands/cleanup.js +1 -0
- package/dist/cli/commands/cleanup.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +2 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/edit.d.ts.map +1 -1
- package/dist/cli/commands/edit.js +3 -0
- package/dist/cli/commands/edit.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +1 -0
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/cli/commands/proficiency.d.ts.map +1 -1
- package/dist/cli/commands/proficiency.js +4 -0
- package/dist/cli/commands/proficiency.js.map +1 -1
- package/dist/cli/commands/rm.d.ts.map +1 -1
- package/dist/cli/commands/rm.js +4 -0
- package/dist/cli/commands/rm.js.map +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +52 -8
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/setup.js +24 -0
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill.d.ts.map +1 -1
- package/dist/cli/commands/skill.js +16 -1
- package/dist/cli/commands/skill.js.map +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +3 -0
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/config/manager.d.ts +67 -13
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +20 -5
- package/dist/config/manager.js.map +1 -1
- package/dist/db/database.d.ts +5 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/database.js +47 -24
- package/dist/db/database.js.map +1 -1
- package/dist/embedding/e5-provider.d.ts +41 -0
- package/dist/embedding/e5-provider.d.ts.map +1 -0
- package/dist/embedding/e5-provider.js +147 -0
- package/dist/embedding/e5-provider.js.map +1 -0
- package/dist/embedding/index.d.ts +1 -0
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +5 -0
- package/dist/embedding/index.js.map +1 -1
- package/dist/extraction/dedup-manager.d.ts +40 -0
- package/dist/extraction/dedup-manager.d.ts.map +1 -0
- package/dist/extraction/dedup-manager.js +148 -0
- package/dist/extraction/dedup-manager.js.map +1 -0
- package/dist/extraction/extractor.d.ts.map +1 -1
- package/dist/extraction/extractor.js +23 -6
- package/dist/extraction/extractor.js.map +1 -1
- package/dist/extraction/filter.d.ts.map +1 -1
- package/dist/extraction/filter.js +3 -0
- package/dist/extraction/filter.js.map +1 -1
- package/dist/extraction/scorer.js +23 -12
- package/dist/extraction/scorer.js.map +1 -1
- package/dist/hooks/client-factory.d.ts +14 -0
- package/dist/hooks/client-factory.d.ts.map +1 -0
- package/dist/hooks/client-factory.js +22 -0
- package/dist/hooks/client-factory.js.map +1 -0
- package/dist/hooks/post-tool-use.d.ts.map +1 -1
- package/dist/hooks/post-tool-use.js +104 -24
- package/dist/hooks/post-tool-use.js.map +1 -1
- package/dist/hooks/pre-compact.d.ts +15 -0
- package/dist/hooks/pre-compact.d.ts.map +1 -0
- package/dist/hooks/pre-compact.js +209 -0
- package/dist/hooks/pre-compact.js.map +1 -0
- package/dist/hooks/session-end.d.ts.map +1 -1
- package/dist/hooks/session-end.js +271 -256
- package/dist/hooks/session-end.js.map +1 -1
- package/dist/hooks/session-start.d.ts +1 -0
- package/dist/hooks/session-start.d.ts.map +1 -1
- package/dist/hooks/session-start.js +221 -138
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/shared.d.ts +10 -0
- package/dist/hooks/shared.d.ts.map +1 -1
- package/dist/hooks/shared.js +64 -0
- package/dist/hooks/shared.js.map +1 -1
- package/dist/hooks/user-prompt-submit.d.ts +15 -0
- package/dist/hooks/user-prompt-submit.d.ts.map +1 -0
- package/dist/hooks/user-prompt-submit.js +240 -0
- package/dist/hooks/user-prompt-submit.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/quality-scorer.d.ts +2 -2
- package/dist/lifecycle/quality-scorer.d.ts.map +1 -1
- package/dist/lifecycle/quality-scorer.js +13 -7
- package/dist/lifecycle/quality-scorer.js.map +1 -1
- package/dist/lifecycle/tiering.d.ts.map +1 -1
- package/dist/lifecycle/tiering.js +11 -1
- package/dist/lifecycle/tiering.js.map +1 -1
- package/dist/proficiency/detection.d.ts +2 -4
- package/dist/proficiency/detection.d.ts.map +1 -1
- package/dist/proficiency/detection.js +11 -15
- package/dist/proficiency/detection.js.map +1 -1
- package/dist/proficiency/tracker.d.ts +2 -2
- package/dist/proficiency/tracker.d.ts.map +1 -1
- package/dist/proficiency/tracker.js +10 -7
- package/dist/proficiency/tracker.js.map +1 -1
- package/dist/search/adaptive-router.d.ts +28 -0
- package/dist/search/adaptive-router.d.ts.map +1 -0
- package/dist/search/adaptive-router.js +93 -0
- package/dist/search/adaptive-router.js.map +1 -0
- package/dist/search/ranker.d.ts +4 -0
- package/dist/search/ranker.d.ts.map +1 -1
- package/dist/search/ranker.js +4 -0
- package/dist/search/ranker.js.map +1 -1
- package/dist/search/reranker.d.ts +48 -0
- package/dist/search/reranker.d.ts.map +1 -0
- package/dist/search/reranker.js +155 -0
- package/dist/search/reranker.js.map +1 -0
- package/dist/skill/evaluator.d.ts +7 -9
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +122 -33
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/types.d.ts +5 -0
- package/dist/skill/types.d.ts.map +1 -1
- package/dist/sync/client.d.ts +26 -1
- package/dist/sync/client.d.ts.map +1 -1
- package/dist/sync/client.js +81 -28
- package/dist/sync/client.js.map +1 -1
- package/dist/sync/queue.d.ts +1 -1
- package/dist/sync/queue.d.ts.map +1 -1
- package/dist/sync/queue.js +20 -28
- package/dist/sync/queue.js.map +1 -1
- package/dist/sync/synchronizer.js +1 -1
- package/dist/sync/synchronizer.js.map +1 -1
- package/dist/sync/team-synchronizer.js.map +1 -1
- package/dist/types/index.d.ts +19 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +11 -3
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,308 +5,322 @@
|
|
|
5
5
|
* 세션 전체를 분석하여 요약 메모리를 생성합니다.
|
|
6
6
|
* autoSync 활성화 시 pending 메모리를 원격 서버로 push합니다.
|
|
7
7
|
*/
|
|
8
|
-
import { getSharedDb, closeSharedDb } from './shared.js';
|
|
8
|
+
import { getSharedDb, closeSharedDb, normalizeHookInput } from './shared.js';
|
|
9
9
|
import { ConfigManager } from '../config/manager.js';
|
|
10
10
|
import { parseSessionFile, findSessionFiles } from '../session/parser.js';
|
|
11
11
|
import { extractMemories, extractMemoriesWithLLM, summarizeSession } from '../extraction/extractor.js';
|
|
12
|
-
import {
|
|
12
|
+
import { createHookClient } from './client-factory.js';
|
|
13
13
|
import { SyncQueue } from '../sync/queue.js';
|
|
14
14
|
import { createLogger } from '../utils/logger.js';
|
|
15
|
+
/** autoSync push + 팀 동기화 */
|
|
16
|
+
async function syncToRemote(_context, config, db, logger) {
|
|
17
|
+
// autoSync push (pending 메모리 일괄 전송)
|
|
18
|
+
if (config.autoSync.enabled && config.autoSync.pushOnSessionEnd && config.server.apiKey) {
|
|
19
|
+
try {
|
|
20
|
+
const client = createHookClient(config);
|
|
21
|
+
if (!client)
|
|
22
|
+
throw new Error('sync client not available');
|
|
23
|
+
const queue = new SyncQueue(db, client);
|
|
24
|
+
const flushResult = await queue.flush({ timeoutMs: config.autoSync.timeoutMs });
|
|
25
|
+
if (flushResult.pushed > 0) {
|
|
26
|
+
console.error(`[a2a] Synced ${flushResult.pushed} memories to server`);
|
|
27
|
+
}
|
|
28
|
+
if (flushResult.errors.length > 0) {
|
|
29
|
+
console.error(`[a2a] Sync errors: ${flushResult.errors.join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (syncErr) {
|
|
33
|
+
logger.warn('Sync push failed', { error: syncErr instanceof Error ? syncErr.message : String(syncErr) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// 팀 동기화 (team 모드)
|
|
37
|
+
if (config.mode === 'team' && config.team && config.server.apiKey) {
|
|
38
|
+
try {
|
|
39
|
+
const { TeamSynchronizer } = await import('../sync/team-synchronizer.js');
|
|
40
|
+
const client = createHookClient(config);
|
|
41
|
+
if (!client)
|
|
42
|
+
throw new Error('team sync client not available');
|
|
43
|
+
const teamSync = new TeamSynchronizer(db, client, config.team.teamPath, config.team.nodeId);
|
|
44
|
+
const teamResult = await teamSync.syncDelta();
|
|
45
|
+
if (teamResult.pushed > 0) {
|
|
46
|
+
console.error(`[a2a] Team synced ${teamResult.pushed} memories`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (teamErr) {
|
|
50
|
+
logger.warn('Team sync failed', { error: teamErr instanceof Error ? teamErr.message : String(teamErr) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
15
54
|
export async function handleSessionEnd(context) {
|
|
16
55
|
const config = new ConfigManager().load();
|
|
17
56
|
const logger = createLogger('SessionEnd', config.logging);
|
|
18
57
|
const startTime = performance.now();
|
|
19
58
|
try {
|
|
20
59
|
logger.info('Hook started', { projectPath: context.projectPath });
|
|
21
|
-
|
|
22
|
-
logger.info('Session summary disabled');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// projectPath 필수 검증
|
|
60
|
+
// projectPath 필수 검증 — 없으면 아무 작업도 불가
|
|
26
61
|
if (!context.projectPath) {
|
|
27
62
|
logger.warn('No projectPath provided');
|
|
28
63
|
return;
|
|
29
64
|
}
|
|
30
|
-
// 세션 파일 찾기
|
|
31
|
-
const sessionFiles = findSessionFiles(context.projectPath);
|
|
32
|
-
if (sessionFiles.length === 0) {
|
|
33
|
-
logger.info('No session files found');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// 가장 최근 수정된 세션 파일 사용
|
|
37
|
-
const latestSession = sessionFiles
|
|
38
|
-
.sort((a, b) => (b.modifiedAt - a.modifiedAt))[0];
|
|
39
|
-
const messages = parseSessionFile(latestSession.filePath);
|
|
40
|
-
// 최소 행동 수 미달 시 스킵
|
|
41
|
-
const toolUseCount = messages.filter((m) => Array.isArray(m.message.content) &&
|
|
42
|
-
m.message.content.some((b) => b.type === 'tool_use')).length;
|
|
43
|
-
logger.info('Session analysis', { messages: messages.length, toolUseCount });
|
|
44
|
-
if (toolUseCount < config.sessionSummary.minActions) {
|
|
45
|
-
logger.info('Tool use count below threshold', { toolUseCount, threshold: config.sessionSummary.minActions });
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
65
|
const db = getSharedDb(config.db.path);
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (existingSession && messages.length <= previousMessageCount) {
|
|
53
|
-
logger.info('No new messages since last extraction', {
|
|
54
|
-
sessionId: latestSession.sessionId,
|
|
55
|
-
previousCount: previousMessageCount,
|
|
56
|
-
currentCount: messages.length,
|
|
57
|
-
});
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// 증분 추출: 이전에 처리된 메시지 이후만 추출
|
|
61
|
-
const messagesToProcess = existingSession
|
|
62
|
-
? messages.slice(previousMessageCount)
|
|
63
|
-
: messages;
|
|
64
|
-
logger.info('Processing messages', {
|
|
65
|
-
total: messages.length,
|
|
66
|
-
new: messagesToProcess.length,
|
|
67
|
-
incremental: !!existingSession,
|
|
68
|
-
});
|
|
69
|
-
// 메모리 추출 (LLM 활성 시 고급 추출)
|
|
70
|
-
let result;
|
|
71
|
-
if (config.llm?.enabled) {
|
|
66
|
+
// ── Phase 1: 세션 추출 (실패해도 Phase 2 진행) ──
|
|
67
|
+
const createdIds = [];
|
|
68
|
+
if (config.sessionSummary.enabled) {
|
|
72
69
|
try {
|
|
73
|
-
|
|
74
|
-
const llmClient = createLLMClient(config.llm);
|
|
75
|
-
result = await extractMemoriesWithLLM(messagesToProcess, context.projectPath, llmClient);
|
|
76
|
-
// 세션 요약 생성 (learning 카테고리)
|
|
77
|
-
const summary = await summarizeSession(messagesToProcess, llmClient);
|
|
78
|
-
if (summary && summary.length > 10) {
|
|
79
|
-
result.memories.push({
|
|
80
|
-
content: summary,
|
|
81
|
-
category: 'learning',
|
|
82
|
-
tier: 'semantic',
|
|
83
|
-
tags: ['session-summary'],
|
|
84
|
-
projectPath: context.projectPath,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
70
|
+
await extractSessionMemories(context, config, db, logger, createdIds);
|
|
87
71
|
}
|
|
88
|
-
catch (
|
|
89
|
-
|
|
90
|
-
logger.warn('LLM extraction failed, using basic', { error: llmErr instanceof Error ? llmErr.message : String(llmErr) });
|
|
91
|
-
result = extractMemories(messagesToProcess, context.projectPath);
|
|
72
|
+
catch (extractErr) {
|
|
73
|
+
logger.warn('Session extraction failed', { error: extractErr instanceof Error ? extractErr.message : String(extractErr) });
|
|
92
74
|
}
|
|
93
75
|
}
|
|
94
76
|
else {
|
|
95
|
-
|
|
77
|
+
logger.info('Session summary disabled');
|
|
96
78
|
}
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// 로컬 임베딩 자동 생성 (~1ms, 동기 처리 가능)
|
|
108
|
-
if (config.embedding?.enabled && config.embedding.provider === 'local') {
|
|
109
|
-
try {
|
|
110
|
-
const { createEmbeddingProvider } = await import('../embedding/index.js');
|
|
111
|
-
const provider = createEmbeddingProvider(config.embedding, config.llm);
|
|
112
|
-
const emb = await Promise.resolve(provider.embed(created.content));
|
|
113
|
-
if (Array.isArray(emb)) {
|
|
114
|
-
db.saveEmbedding(created.id, emb);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// 임베딩 생성 실패 무시 — 다음 세션에서 재시도
|
|
119
|
-
}
|
|
120
|
-
}
|
|
79
|
+
// ── Phase 2: 항상 실행 — Sync, Cleanup (세션 추출과 독립) ──
|
|
80
|
+
// 숙련도 추적 + 서버 Push (Phase 5)
|
|
81
|
+
// createdIds.length > 0 게이트 제거: PostToolUse가 세션 중 이미 메모리를 생성했으므로
|
|
82
|
+
// SessionEnd의 createdIds(세션파일 추출분)과 무관하게 proficiency 평가 수행
|
|
83
|
+
if (config.proficiency?.enabled) {
|
|
84
|
+
// 세션 중 생성된 모든 메모리를 DB에서 조회하여 평가 대상으로 사용
|
|
85
|
+
const sessionMemoryIds = createdIds.length > 0
|
|
86
|
+
? createdIds
|
|
87
|
+
: db.listMemories({ limit: 50 }).map(m => m.id);
|
|
88
|
+
await trackProficiency(context, config, db, logger, sessionMemoryIds);
|
|
121
89
|
}
|
|
122
|
-
//
|
|
123
|
-
|
|
90
|
+
// 원격 동기화 (autoSync push + 팀 동기화)
|
|
91
|
+
await syncToRemote(context, config, db, logger);
|
|
92
|
+
// 라이프사이클 정리 (설정 활성화 시)
|
|
93
|
+
if (config.lifecycle?.cleanupOnSessionEnd) {
|
|
124
94
|
try {
|
|
125
|
-
const {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return mem ? mem.content : '';
|
|
130
|
-
}).filter(Boolean);
|
|
131
|
-
if (contents.length > 0) {
|
|
132
|
-
const embeddings = await Promise.resolve(provider.embedBatch(contents));
|
|
133
|
-
for (let i = 0; i < createdIds.length; i++) {
|
|
134
|
-
if (embeddings[i]) {
|
|
135
|
-
db.saveEmbedding(createdIds[i], embeddings[i]);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
95
|
+
const { cleanupMemories } = await import('../lifecycle/index.js');
|
|
96
|
+
const cleanupResult = cleanupMemories(db, config.lifecycle);
|
|
97
|
+
if (cleanupResult.total > 0) {
|
|
98
|
+
console.error(`[a2a] Cleaned up ${cleanupResult.total} memories (${cleanupResult.expired} expired, ${cleanupResult.lowQuality} low-quality)`);
|
|
138
99
|
}
|
|
100
|
+
logger.info('Cleanup done', { total: cleanupResult.total });
|
|
139
101
|
}
|
|
140
|
-
catch {
|
|
141
|
-
|
|
102
|
+
catch (cleanupErr) {
|
|
103
|
+
logger.warn('Cleanup failed', { error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr) });
|
|
142
104
|
}
|
|
143
105
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
106
|
+
logger.info('Hook completed', { durationMs: Math.round(performance.now() - startTime) });
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
logger.flush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 세션 파일에서 메모리 추출 (Phase 1 내부)
|
|
114
|
+
*/
|
|
115
|
+
async function extractSessionMemories(context, config, db, logger, createdIds) {
|
|
116
|
+
const sessionFiles = findSessionFiles(context.projectPath);
|
|
117
|
+
if (sessionFiles.length === 0) {
|
|
118
|
+
logger.info('No session files found');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const latestSession = sessionFiles
|
|
122
|
+
.sort((a, b) => (b.modifiedAt - a.modifiedAt))[0];
|
|
123
|
+
const messages = parseSessionFile(latestSession.filePath);
|
|
124
|
+
const toolUseCount = messages.filter((m) => Array.isArray(m.message.content) &&
|
|
125
|
+
m.message.content.some((b) => b.type === 'tool_use')).length;
|
|
126
|
+
logger.info('Session analysis', { messages: messages.length, toolUseCount });
|
|
127
|
+
if (toolUseCount < config.sessionSummary.minActions) {
|
|
128
|
+
logger.info('Tool use count below threshold', { toolUseCount, threshold: config.sessionSummary.minActions });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// 이미 처리된 세션 확인 — 새 메시지가 있으면 증분 추출
|
|
132
|
+
const existingSession = db.getSession(latestSession.sessionId);
|
|
133
|
+
const previousMessageCount = existingSession?.messageCount ?? 0;
|
|
134
|
+
if (existingSession && messages.length <= previousMessageCount) {
|
|
135
|
+
logger.info('No new messages since last extraction', {
|
|
136
|
+
sessionId: latestSession.sessionId,
|
|
137
|
+
previousCount: previousMessageCount,
|
|
138
|
+
currentCount: messages.length,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const messagesToProcess = existingSession
|
|
143
|
+
? messages.slice(previousMessageCount)
|
|
144
|
+
: messages;
|
|
145
|
+
logger.info('Processing messages', {
|
|
146
|
+
total: messages.length,
|
|
147
|
+
new: messagesToProcess.length,
|
|
148
|
+
incremental: !!existingSession,
|
|
149
|
+
});
|
|
150
|
+
// 메모리 추출 (LLM 활성 시 고급 추출)
|
|
151
|
+
let result;
|
|
152
|
+
if (config.llm?.enabled) {
|
|
153
|
+
try {
|
|
154
|
+
const { createLLMClient } = await import('../llm/index.js');
|
|
155
|
+
const llmClient = createLLMClient(config.llm);
|
|
156
|
+
result = await extractMemoriesWithLLM(messagesToProcess, context.projectPath, llmClient);
|
|
157
|
+
const summary = await summarizeSession(messagesToProcess, llmClient);
|
|
158
|
+
if (summary && summary.length > 10) {
|
|
159
|
+
result.memories.push({
|
|
160
|
+
content: summary,
|
|
161
|
+
category: 'learning',
|
|
162
|
+
tier: 'semantic',
|
|
163
|
+
tags: ['session-summary'],
|
|
164
|
+
projectPath: context.projectPath,
|
|
165
|
+
});
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
catch (llmErr) {
|
|
169
|
+
logger.warn('LLM extraction failed, using basic', { error: llmErr instanceof Error ? llmErr.message : String(llmErr) });
|
|
170
|
+
result = extractMemories(messagesToProcess, context.projectPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
result = extractMemories(messagesToProcess, context.projectPath);
|
|
175
|
+
}
|
|
176
|
+
logger.info('Memories extracted', { count: result.memories.length });
|
|
177
|
+
// DB에 저장 + sync_status pending 설정
|
|
178
|
+
for (const memory of result.memories) {
|
|
179
|
+
const created = db.createMemory({
|
|
180
|
+
...memory,
|
|
181
|
+
sessionId: latestSession.sessionId,
|
|
182
|
+
});
|
|
183
|
+
db.setSyncStatus(created.id, null, 'pending');
|
|
184
|
+
createdIds.push(created.id);
|
|
185
|
+
if (config.embedding?.enabled && config.embedding.provider === 'local') {
|
|
170
186
|
try {
|
|
171
|
-
const {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const sessionMemories = createdIds
|
|
177
|
-
.map(id => {
|
|
178
|
-
const mem = db.getMemory(id);
|
|
179
|
-
if (!mem)
|
|
180
|
-
return null;
|
|
181
|
-
const emb = db.getEmbedding(id);
|
|
182
|
-
return {
|
|
183
|
-
content: mem.content,
|
|
184
|
-
tags: mem.tags ?? [],
|
|
185
|
-
embedding: emb ?? undefined,
|
|
186
|
-
};
|
|
187
|
-
})
|
|
188
|
-
.filter((m) => m !== null);
|
|
189
|
-
for (const skill of skills) {
|
|
190
|
-
const skillInfo = {
|
|
191
|
-
id: skill.id,
|
|
192
|
-
content: skill.content,
|
|
193
|
-
tags: skill.tags ?? [],
|
|
194
|
-
embedding: skill.embedding ?? db.getEmbedding(skill.id) ?? undefined,
|
|
195
|
-
};
|
|
196
|
-
const detection = detectSkillPractice(skillInfo, sessionMemories);
|
|
197
|
-
if (!detection)
|
|
198
|
-
continue;
|
|
199
|
-
// 로컬 숙련도 기록
|
|
200
|
-
const levelResult = tracker.recordPractice(skill.id, detection.outcome, detection.difficulty, detection.contextTags, context.sessionId);
|
|
201
|
-
if (levelResult.levelChanged) {
|
|
202
|
-
console.error(`[a2a] Proficiency: "${skill.content.split('\n')[0].slice(0, 50)}" level ${levelResult.previousLevel} → ${levelResult.newLevel}`);
|
|
203
|
-
}
|
|
204
|
-
// 서버 push용 이벤트 수집
|
|
205
|
-
detectedProficiencyEvents.push({
|
|
206
|
-
skillMemoryId: skill.id,
|
|
207
|
-
outcome: detection.outcome,
|
|
208
|
-
difficulty: detection.difficulty,
|
|
209
|
-
contextTags: detection.contextTags,
|
|
210
|
-
});
|
|
187
|
+
const { createEmbeddingProvider } = await import('../embedding/index.js');
|
|
188
|
+
const provider = createEmbeddingProvider(config.embedding, config.llm);
|
|
189
|
+
const emb = await Promise.resolve(provider.embed(created.content));
|
|
190
|
+
if (Array.isArray(emb)) {
|
|
191
|
+
db.saveEmbedding(created.id, emb);
|
|
211
192
|
}
|
|
212
193
|
}
|
|
213
|
-
catch
|
|
214
|
-
|
|
194
|
+
catch {
|
|
195
|
+
// 임베딩 생성 실패 무시
|
|
215
196
|
}
|
|
216
197
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
context_tags: event.contextTags,
|
|
233
|
-
session_id: context.sessionId,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
logger.warn('Failed to push proficiency event', { error: String(err) });
|
|
238
|
-
}
|
|
198
|
+
}
|
|
199
|
+
// OpenAI 임베딩 일괄 처리
|
|
200
|
+
if (config.embedding?.enabled && config.embedding.provider === 'openai' && config.llm?.apiKey && createdIds.length > 0) {
|
|
201
|
+
try {
|
|
202
|
+
const { createEmbeddingProvider } = await import('../embedding/index.js');
|
|
203
|
+
const provider = createEmbeddingProvider(config.embedding, config.llm);
|
|
204
|
+
const contents = createdIds.map(id => {
|
|
205
|
+
const mem = db.getMemory(id);
|
|
206
|
+
return mem ? mem.content : '';
|
|
207
|
+
}).filter(Boolean);
|
|
208
|
+
if (contents.length > 0) {
|
|
209
|
+
const embeddings = await Promise.resolve(provider.embedBatch(contents));
|
|
210
|
+
for (let i = 0; i < createdIds.length; i++) {
|
|
211
|
+
if (embeddings[i]) {
|
|
212
|
+
db.saveEmbedding(createdIds[i], embeddings[i]);
|
|
239
213
|
}
|
|
240
|
-
logger.info('Proficiency events pushed to server', { count: detectedProficiencyEvents.length });
|
|
241
|
-
console.error(`[a2a] Proficiency: ${detectedProficiencyEvents.length} event(s) synced to server`);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
logger.debug('Server not reachable, skipping proficiency push');
|
|
245
214
|
}
|
|
246
215
|
}
|
|
247
|
-
catch (err) {
|
|
248
|
-
logger.warn('Proficiency server push failed', { error: String(err) });
|
|
249
|
-
}
|
|
250
216
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const client = new A2AClient({
|
|
255
|
-
baseUrl: config.server.url,
|
|
256
|
-
apiKey: config.server.apiKey,
|
|
257
|
-
});
|
|
258
|
-
const queue = new SyncQueue(db, client);
|
|
259
|
-
const flushResult = await queue.flush({ timeoutMs: config.autoSync.timeoutMs }); // hook: config 값 사용
|
|
260
|
-
if (flushResult.pushed > 0) {
|
|
261
|
-
console.error(`[a2a] Synced ${flushResult.pushed} memories to server`);
|
|
262
|
-
}
|
|
263
|
-
if (flushResult.errors.length > 0) {
|
|
264
|
-
console.error(`[a2a] Sync errors: ${flushResult.errors.join(', ')}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
catch (syncErr) {
|
|
268
|
-
// sync 실패는 무시 — 다음 세션에서 재시도
|
|
269
|
-
logger.warn('Sync push failed', { error: syncErr instanceof Error ? syncErr.message : String(syncErr) });
|
|
270
|
-
}
|
|
217
|
+
catch {
|
|
218
|
+
// 일괄 임베딩 실패 무시
|
|
271
219
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
220
|
+
}
|
|
221
|
+
// 세션 기록
|
|
222
|
+
db.saveSession(latestSession.sessionId, context.projectPath, messages.length);
|
|
223
|
+
if (result.memories.length > 0) {
|
|
224
|
+
const mode = existingSession ? ' (incremental)' : '';
|
|
225
|
+
console.error(`[a2a] Session summary${mode}: ${result.memories.length} memories extracted from ${messagesToProcess.length} messages`);
|
|
226
|
+
}
|
|
227
|
+
// 스킬 결정화 평가 (N 세션마다)
|
|
228
|
+
if (config.skillConversion?.enabled) {
|
|
229
|
+
try {
|
|
230
|
+
const sessionCount = db.getSessionCount(context.projectPath);
|
|
231
|
+
if (sessionCount > 0 && sessionCount % config.skillConversion.evaluationInterval === 0) {
|
|
232
|
+
const { SkillEvaluator } = await import('../skill/evaluator.js');
|
|
233
|
+
const evaluator = new SkillEvaluator(db, config);
|
|
234
|
+
// Taxonomy 분류를 위한 A2AClient (선택적)
|
|
235
|
+
const taxonomyClient = createHookClient(config, 10000) ?? undefined; // taxonomy 분류는 빠르게 (10초)
|
|
236
|
+
const evalResult = await evaluator.evaluate(context.projectPath, taxonomyClient);
|
|
237
|
+
if (evalResult.created > 0) {
|
|
238
|
+
console.error(`[a2a] Skill crystallization: ${evalResult.created} new skills created`);
|
|
284
239
|
}
|
|
285
240
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
241
|
+
}
|
|
242
|
+
catch (skillErr) {
|
|
243
|
+
logger.warn('Skill evaluation failed', { error: skillErr instanceof Error ? skillErr.message : String(skillErr) });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 숙련도 추적 + 서버 Push
|
|
249
|
+
*/
|
|
250
|
+
async function trackProficiency(context, config, db, logger, createdIds) {
|
|
251
|
+
const detectedEvents = [];
|
|
252
|
+
try {
|
|
253
|
+
const { ProficiencyTracker } = await import('../proficiency/tracker.js');
|
|
254
|
+
const { detectSkillPractice } = await import('../proficiency/detection.js');
|
|
255
|
+
const tracker = new ProficiencyTracker(db, config.proficiency);
|
|
256
|
+
const skills = db.listMemories({ category: 'skill', limit: 100 });
|
|
257
|
+
const sessionMemories = createdIds
|
|
258
|
+
.map(id => {
|
|
259
|
+
const mem = db.getMemory(id);
|
|
260
|
+
if (!mem)
|
|
261
|
+
return null;
|
|
262
|
+
const emb = db.getEmbedding(id);
|
|
263
|
+
return {
|
|
264
|
+
content: mem.content,
|
|
265
|
+
tags: mem.tags ?? [],
|
|
266
|
+
embedding: emb ?? undefined,
|
|
267
|
+
};
|
|
268
|
+
})
|
|
269
|
+
.filter((m) => m !== null);
|
|
270
|
+
for (const skill of skills) {
|
|
271
|
+
const skillInfo = {
|
|
272
|
+
id: skill.id,
|
|
273
|
+
content: skill.content,
|
|
274
|
+
tags: skill.tags ?? [],
|
|
275
|
+
embedding: skill.embedding ?? db.getEmbedding(skill.id) ?? undefined,
|
|
276
|
+
};
|
|
277
|
+
const detection = detectSkillPractice(skillInfo, sessionMemories);
|
|
278
|
+
if (!detection)
|
|
279
|
+
continue;
|
|
280
|
+
const levelResult = tracker.recordPractice(skill.id, detection.outcome, detection.difficulty, detection.contextTags, context.sessionId);
|
|
281
|
+
if (levelResult.levelChanged) {
|
|
282
|
+
console.error(`[a2a] Proficiency: "${skill.content.split('\n')[0].slice(0, 50)}" level ${levelResult.previousLevel} → ${levelResult.newLevel}`);
|
|
289
283
|
}
|
|
284
|
+
detectedEvents.push({
|
|
285
|
+
skillMemoryId: skill.id,
|
|
286
|
+
outcome: detection.outcome,
|
|
287
|
+
difficulty: detection.difficulty,
|
|
288
|
+
contextTags: detection.contextTags,
|
|
289
|
+
});
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
}
|
|
292
|
+
catch (profErr) {
|
|
293
|
+
logger.warn('Proficiency tracking failed', { error: profErr instanceof Error ? profErr.message : String(profErr) });
|
|
294
|
+
}
|
|
295
|
+
// Proficiency Server Push
|
|
296
|
+
if (detectedEvents.length > 0 && config.server?.apiKey && config.server?.url) {
|
|
297
|
+
try {
|
|
298
|
+
const pushClient = createHookClient(config);
|
|
299
|
+
if (!pushClient)
|
|
300
|
+
return;
|
|
301
|
+
const connected = await pushClient.testConnection();
|
|
302
|
+
if (connected) {
|
|
303
|
+
for (const event of detectedEvents) {
|
|
304
|
+
try {
|
|
305
|
+
await pushClient.postProficiencyEvent({
|
|
306
|
+
skill_memory_id: event.skillMemoryId,
|
|
307
|
+
outcome: event.outcome,
|
|
308
|
+
difficulty: event.difficulty,
|
|
309
|
+
context_tags: event.contextTags,
|
|
310
|
+
session_id: context.sessionId,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
logger.warn('Failed to push proficiency event', { error: String(err) });
|
|
315
|
+
}
|
|
298
316
|
}
|
|
299
|
-
logger.info('
|
|
300
|
-
|
|
301
|
-
catch (cleanupErr) {
|
|
302
|
-
// cleanup 실패 무시
|
|
303
|
-
logger.warn('Cleanup failed', { error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr) });
|
|
317
|
+
logger.info('Proficiency events pushed to server', { count: detectedEvents.length });
|
|
318
|
+
console.error(`[a2a] Proficiency: ${detectedEvents.length} event(s) synced to server`);
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
logger.flush();
|
|
321
|
+
catch (err) {
|
|
322
|
+
logger.warn('Proficiency server push failed', { error: String(err) });
|
|
323
|
+
}
|
|
310
324
|
}
|
|
311
325
|
}
|
|
312
326
|
/**
|
|
@@ -323,7 +337,8 @@ export async function main() {
|
|
|
323
337
|
process.exit(0);
|
|
324
338
|
}
|
|
325
339
|
try {
|
|
326
|
-
const
|
|
340
|
+
const raw = JSON.parse(input);
|
|
341
|
+
const context = normalizeHookInput(raw);
|
|
327
342
|
await handleSessionEnd(context);
|
|
328
343
|
}
|
|
329
344
|
catch (err) {
|