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.
Files changed (142) hide show
  1. package/dist/cli/commands/add.d.ts.map +1 -1
  2. package/dist/cli/commands/add.js +1 -0
  3. package/dist/cli/commands/add.js.map +1 -1
  4. package/dist/cli/commands/claude-sync.d.ts.map +1 -1
  5. package/dist/cli/commands/claude-sync.js +1 -0
  6. package/dist/cli/commands/claude-sync.js.map +1 -1
  7. package/dist/cli/commands/cleanup.d.ts.map +1 -1
  8. package/dist/cli/commands/cleanup.js +1 -0
  9. package/dist/cli/commands/cleanup.js.map +1 -1
  10. package/dist/cli/commands/config.d.ts.map +1 -1
  11. package/dist/cli/commands/config.js +2 -0
  12. package/dist/cli/commands/config.js.map +1 -1
  13. package/dist/cli/commands/edit.d.ts.map +1 -1
  14. package/dist/cli/commands/edit.js +3 -0
  15. package/dist/cli/commands/edit.js.map +1 -1
  16. package/dist/cli/commands/embed.d.ts.map +1 -1
  17. package/dist/cli/commands/embed.js +1 -0
  18. package/dist/cli/commands/embed.js.map +1 -1
  19. package/dist/cli/commands/proficiency.d.ts.map +1 -1
  20. package/dist/cli/commands/proficiency.js +4 -0
  21. package/dist/cli/commands/proficiency.js.map +1 -1
  22. package/dist/cli/commands/rm.d.ts.map +1 -1
  23. package/dist/cli/commands/rm.js +4 -0
  24. package/dist/cli/commands/rm.js.map +1 -1
  25. package/dist/cli/commands/search.d.ts.map +1 -1
  26. package/dist/cli/commands/search.js +52 -8
  27. package/dist/cli/commands/search.js.map +1 -1
  28. package/dist/cli/commands/setup.js +24 -0
  29. package/dist/cli/commands/setup.js.map +1 -1
  30. package/dist/cli/commands/skill.d.ts.map +1 -1
  31. package/dist/cli/commands/skill.js +16 -1
  32. package/dist/cli/commands/skill.js.map +1 -1
  33. package/dist/cli/commands/sync.d.ts.map +1 -1
  34. package/dist/cli/commands/sync.js +3 -0
  35. package/dist/cli/commands/sync.js.map +1 -1
  36. package/dist/config/manager.d.ts +67 -13
  37. package/dist/config/manager.d.ts.map +1 -1
  38. package/dist/config/manager.js +20 -5
  39. package/dist/config/manager.js.map +1 -1
  40. package/dist/db/database.d.ts +5 -0
  41. package/dist/db/database.d.ts.map +1 -1
  42. package/dist/db/database.js +47 -24
  43. package/dist/db/database.js.map +1 -1
  44. package/dist/embedding/e5-provider.d.ts +41 -0
  45. package/dist/embedding/e5-provider.d.ts.map +1 -0
  46. package/dist/embedding/e5-provider.js +147 -0
  47. package/dist/embedding/e5-provider.js.map +1 -0
  48. package/dist/embedding/index.d.ts +1 -0
  49. package/dist/embedding/index.d.ts.map +1 -1
  50. package/dist/embedding/index.js +5 -0
  51. package/dist/embedding/index.js.map +1 -1
  52. package/dist/extraction/dedup-manager.d.ts +40 -0
  53. package/dist/extraction/dedup-manager.d.ts.map +1 -0
  54. package/dist/extraction/dedup-manager.js +148 -0
  55. package/dist/extraction/dedup-manager.js.map +1 -0
  56. package/dist/extraction/extractor.d.ts.map +1 -1
  57. package/dist/extraction/extractor.js +23 -6
  58. package/dist/extraction/extractor.js.map +1 -1
  59. package/dist/extraction/filter.d.ts.map +1 -1
  60. package/dist/extraction/filter.js +3 -0
  61. package/dist/extraction/filter.js.map +1 -1
  62. package/dist/extraction/scorer.js +23 -12
  63. package/dist/extraction/scorer.js.map +1 -1
  64. package/dist/hooks/client-factory.d.ts +14 -0
  65. package/dist/hooks/client-factory.d.ts.map +1 -0
  66. package/dist/hooks/client-factory.js +22 -0
  67. package/dist/hooks/client-factory.js.map +1 -0
  68. package/dist/hooks/post-tool-use.d.ts.map +1 -1
  69. package/dist/hooks/post-tool-use.js +104 -24
  70. package/dist/hooks/post-tool-use.js.map +1 -1
  71. package/dist/hooks/pre-compact.d.ts +15 -0
  72. package/dist/hooks/pre-compact.d.ts.map +1 -0
  73. package/dist/hooks/pre-compact.js +209 -0
  74. package/dist/hooks/pre-compact.js.map +1 -0
  75. package/dist/hooks/session-end.d.ts.map +1 -1
  76. package/dist/hooks/session-end.js +271 -256
  77. package/dist/hooks/session-end.js.map +1 -1
  78. package/dist/hooks/session-start.d.ts +1 -0
  79. package/dist/hooks/session-start.d.ts.map +1 -1
  80. package/dist/hooks/session-start.js +221 -138
  81. package/dist/hooks/session-start.js.map +1 -1
  82. package/dist/hooks/shared.d.ts +10 -0
  83. package/dist/hooks/shared.d.ts.map +1 -1
  84. package/dist/hooks/shared.js +64 -0
  85. package/dist/hooks/shared.js.map +1 -1
  86. package/dist/hooks/user-prompt-submit.d.ts +15 -0
  87. package/dist/hooks/user-prompt-submit.d.ts.map +1 -0
  88. package/dist/hooks/user-prompt-submit.js +240 -0
  89. package/dist/hooks/user-prompt-submit.js.map +1 -0
  90. package/dist/index.d.ts +11 -3
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +8 -2
  93. package/dist/index.js.map +1 -1
  94. package/dist/lifecycle/quality-scorer.d.ts +2 -2
  95. package/dist/lifecycle/quality-scorer.d.ts.map +1 -1
  96. package/dist/lifecycle/quality-scorer.js +13 -7
  97. package/dist/lifecycle/quality-scorer.js.map +1 -1
  98. package/dist/lifecycle/tiering.d.ts.map +1 -1
  99. package/dist/lifecycle/tiering.js +11 -1
  100. package/dist/lifecycle/tiering.js.map +1 -1
  101. package/dist/proficiency/detection.d.ts +2 -4
  102. package/dist/proficiency/detection.d.ts.map +1 -1
  103. package/dist/proficiency/detection.js +11 -15
  104. package/dist/proficiency/detection.js.map +1 -1
  105. package/dist/proficiency/tracker.d.ts +2 -2
  106. package/dist/proficiency/tracker.d.ts.map +1 -1
  107. package/dist/proficiency/tracker.js +10 -7
  108. package/dist/proficiency/tracker.js.map +1 -1
  109. package/dist/search/adaptive-router.d.ts +28 -0
  110. package/dist/search/adaptive-router.d.ts.map +1 -0
  111. package/dist/search/adaptive-router.js +93 -0
  112. package/dist/search/adaptive-router.js.map +1 -0
  113. package/dist/search/ranker.d.ts +4 -0
  114. package/dist/search/ranker.d.ts.map +1 -1
  115. package/dist/search/ranker.js +4 -0
  116. package/dist/search/ranker.js.map +1 -1
  117. package/dist/search/reranker.d.ts +48 -0
  118. package/dist/search/reranker.d.ts.map +1 -0
  119. package/dist/search/reranker.js +155 -0
  120. package/dist/search/reranker.js.map +1 -0
  121. package/dist/skill/evaluator.d.ts +7 -9
  122. package/dist/skill/evaluator.d.ts.map +1 -1
  123. package/dist/skill/evaluator.js +122 -33
  124. package/dist/skill/evaluator.js.map +1 -1
  125. package/dist/skill/types.d.ts +5 -0
  126. package/dist/skill/types.d.ts.map +1 -1
  127. package/dist/sync/client.d.ts +26 -1
  128. package/dist/sync/client.d.ts.map +1 -1
  129. package/dist/sync/client.js +81 -28
  130. package/dist/sync/client.js.map +1 -1
  131. package/dist/sync/queue.d.ts +1 -1
  132. package/dist/sync/queue.d.ts.map +1 -1
  133. package/dist/sync/queue.js +20 -28
  134. package/dist/sync/queue.js.map +1 -1
  135. package/dist/sync/synchronizer.js +1 -1
  136. package/dist/sync/synchronizer.js.map +1 -1
  137. package/dist/sync/team-synchronizer.js.map +1 -1
  138. package/dist/types/index.d.ts +19 -2
  139. package/dist/types/index.d.ts.map +1 -1
  140. package/dist/types/index.js +11 -3
  141. package/dist/types/index.js.map +1 -1
  142. 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 { A2AClient } from '../sync/client.js';
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
- if (!config.sessionSummary.enabled) {
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 existingSession = db.getSession(latestSession.sessionId);
51
- const previousMessageCount = existingSession?.messageCount ?? 0;
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
- const { createLLMClient } = await import('../llm/index.js');
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 (llmErr) {
89
- // LLM 실패 기본 추출
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
- result = extractMemories(messagesToProcess, context.projectPath);
77
+ logger.info('Session summary disabled');
96
78
  }
97
- logger.info('Memories extracted', { count: result.memories.length });
98
- // DB에 저장 + sync_status pending 설정
99
- const createdIds = [];
100
- for (const memory of result.memories) {
101
- const created = db.createMemory({
102
- ...memory,
103
- sessionId: latestSession.sessionId,
104
- });
105
- db.setSyncStatus(created.id, null, 'pending');
106
- createdIds.push(created.id);
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
- // OpenAI 임베딩은 일괄 처리 (네트워크 비용 절감)
123
- if (config.embedding?.enabled && config.embedding.provider === 'openai' && config.llm?.apiKey && createdIds.length > 0) {
90
+ // 원격 동기화 (autoSync push + 동기화)
91
+ await syncToRemote(context, config, db, logger);
92
+ // 라이프사이클 정리 (설정 활성화 시)
93
+ if (config.lifecycle?.cleanupOnSessionEnd) {
124
94
  try {
125
- const { createEmbeddingProvider } = await import('../embedding/index.js');
126
- const provider = createEmbeddingProvider(config.embedding, config.llm);
127
- const contents = createdIds.map(id => {
128
- const mem = db.getMemory(id);
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
- db.saveSession(latestSession.sessionId, context.projectPath, messages.length);
146
- if (result.memories.length > 0) {
147
- const mode = existingSession ? ' (incremental)' : '';
148
- console.error(`[a2a] Session summary${mode}: ${result.memories.length} memories extracted from ${messagesToProcess.length} messages`);
149
- }
150
- // 스킬 결정화 평가 (N 세션마다)
151
- if (config.skillConversion?.enabled) {
152
- try {
153
- const sessionCount = db.getSessionCount(context.projectPath);
154
- if (sessionCount > 0 && sessionCount % config.skillConversion.evaluationInterval === 0) {
155
- const { SkillEvaluator } = await import('../skill/evaluator.js');
156
- const evaluator = new SkillEvaluator(db, config);
157
- const evalResult = await evaluator.evaluate(context.projectPath);
158
- if (evalResult.created > 0) {
159
- console.error(`[a2a] Skill crystallization: ${evalResult.created} new skills created`);
160
- }
161
- }
162
- }
163
- catch (skillErr) {
164
- logger.warn('Skill evaluation failed', { error: skillErr instanceof Error ? skillErr.message : String(skillErr) });
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
- // 숙련도 추적 (Phase 5 — Multi-signal detection)
168
- const detectedProficiencyEvents = [];
169
- if (config.proficiency?.enabled) {
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 { ProficiencyTracker } = await import('../proficiency/tracker.js');
172
- const { detectSkillPractice } = await import('../proficiency/detection.js');
173
- const tracker = new ProficiencyTracker(db, config.proficiency);
174
- const skills = db.listMemories({ category: 'skill', limit: 100 });
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 (profErr) {
214
- logger.warn('Proficiency tracking failed', { error: profErr instanceof Error ? profErr.message : String(profErr) });
194
+ catch {
195
+ // 임베딩 생성 실패 무시
215
196
  }
216
197
  }
217
- // === Proficiency Server Push ===
218
- if (detectedProficiencyEvents.length > 0 && config.server?.apiKey && config.server?.url) {
219
- try {
220
- const pushClient = new A2AClient({
221
- baseUrl: config.server.url,
222
- apiKey: config.server.apiKey,
223
- });
224
- const connected = await pushClient.testConnection();
225
- if (connected) {
226
- for (const event of detectedProficiencyEvents) {
227
- try {
228
- await pushClient.postProficiencyEvent({
229
- skill_memory_id: event.skillMemoryId,
230
- outcome: event.outcome,
231
- difficulty: event.difficulty,
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
- // autoSync push (pending 메모리 일괄 전송)
252
- if (config.autoSync.enabled && config.autoSync.pushOnSessionEnd && config.server.apiKey) {
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
- // 팀 동기화 (team 모드)
273
- if (config.mode === 'team' && config.team && config.server.apiKey) {
274
- try {
275
- const { TeamSynchronizer } = await import('../sync/team-synchronizer.js');
276
- const client = new A2AClient({
277
- baseUrl: config.server.url,
278
- apiKey: config.server.apiKey,
279
- });
280
- const teamSync = new TeamSynchronizer(db, client, config.team.teamPath, config.team.nodeId);
281
- const teamResult = await teamSync.syncDelta();
282
- if (teamResult.pushed > 0) {
283
- console.error(`[a2a] Team synced ${teamResult.pushed} memories`);
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
- catch (teamErr) {
287
- // sync 실패 무시
288
- logger.warn('Team sync failed', { error: teamErr instanceof Error ? teamErr.message : String(teamErr) });
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
- if (config.lifecycle?.cleanupOnSessionEnd) {
293
- try {
294
- const { cleanupMemories } = await import('../lifecycle/index.js');
295
- const cleanupResult = cleanupMemories(db, config.lifecycle);
296
- if (cleanupResult.total > 0) {
297
- console.error(`[a2a] Cleaned up ${cleanupResult.total} memories (${cleanupResult.expired} expired, ${cleanupResult.lowQuality} low-quality)`);
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('Cleanup done', { total: cleanupResult.total });
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
- logger.info('Hook completed', { durationMs: Math.round(performance.now() - startTime) });
307
- }
308
- finally {
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 context = JSON.parse(input);
340
+ const raw = JSON.parse(input);
341
+ const context = normalizeHookInput(raw);
327
342
  await handleSessionEnd(context);
328
343
  }
329
344
  catch (err) {