claude-memory-layer 1.0.24 → 1.0.25
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/.claude/settings.local.json +15 -1
- package/dist/cli/index.js +152 -969
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +31 -66
- package/dist/core/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +181 -967
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +148 -965
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +148 -965
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +150 -965
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +156 -965
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +150 -967
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +148 -965
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +148 -965
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +148 -965
- package/dist/services/memory-service.js.map +4 -4
- package/memory/agent_response/uncategorized/2026-03-04.md +217 -1
- package/memory/session_summary/uncategorized/2026-03-04.md +20 -1
- package/memory/tool_observation/uncategorized/2026-03-04.md +237 -1
- package/memory/user_prompt/uncategorized/2026-03-04.md +185 -1
- package/package.json +1 -2
- package/specs/memory-utilization-improvements/context.md +145 -0
- package/specs/memory-utilization-improvements/plan.md +361 -0
- package/specs/memory-utilization-improvements/spec.md +308 -0
- package/specs/optional-duckdb/context.md +77 -0
- package/specs/optional-duckdb/plan.md +142 -0
- package/specs/optional-duckdb/spec.md +35 -0
- package/src/core/db-wrapper.ts +18 -73
- package/src/core/sqlite-event-store.ts +24 -0
- package/src/hooks/post-tool-use.ts +25 -0
- package/src/hooks/session-start.ts +4 -0
- package/src/hooks/stop.ts +14 -0
- package/src/services/memory-service.ts +62 -58
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Context: DuckDB 설치 실패 문제
|
|
2
|
+
|
|
3
|
+
## 에러 요약
|
|
4
|
+
|
|
5
|
+
`npm install -g claude-memory-layer` 실행 시 특정 환경에서 설치 실패:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm error code 1
|
|
9
|
+
npm error path .../node_modules/duckdb
|
|
10
|
+
npm error command failed: node-pre-gyp install --fallback-to-build
|
|
11
|
+
npm error node-pre-gyp ERR! install response status 404 Not Found
|
|
12
|
+
on https://npm.duckdb.org/duckdb/duckdb-v0.10.2-node-v137-darwin-arm64.tar.gz
|
|
13
|
+
npm error gyp ERR! build error (C++ template compile errors)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 실패 환경
|
|
17
|
+
|
|
18
|
+
- **OS**: macOS Darwin 25.2.0 (arm64)
|
|
19
|
+
- **Node.js**: v24.13.1 (ABI v137)
|
|
20
|
+
- **duckdb 버전**: 0.10.2 (`^0.10.0`)
|
|
21
|
+
|
|
22
|
+
## 근본 원인
|
|
23
|
+
|
|
24
|
+
### 1. Pre-built binary 없음
|
|
25
|
+
`duckdb@0.10.2`는 Node.js v24 (ABI v137)용 pre-built binary를 제공하지 않는다.
|
|
26
|
+
Node.js v24는 2024년 후반에 release된 최신 버전이고, duckdb@0.10.x 시리즈는 이를 지원하지 않는다.
|
|
27
|
+
|
|
28
|
+
### 2. Source 컴파일 실패
|
|
29
|
+
fallback으로 source 컴파일을 시도하지만, macOS 최신 Clang 컴파일러가
|
|
30
|
+
duckdb@0.10.x의 C++ template syntax를 거부:
|
|
31
|
+
|
|
32
|
+
```cpp
|
|
33
|
+
// 에러 발생 패턴
|
|
34
|
+
STATE::template ReadValue(...) // Clang: "template argument list expected"
|
|
35
|
+
OP::template Assign(state, input)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
이 C++ 코드는 오래된 컴파일러 표준에서는 동작했지만, 최신 Clang (macOS 26 beta 포함)에서 엄격히 거부됨.
|
|
39
|
+
|
|
40
|
+
## DuckDB의 역할
|
|
41
|
+
|
|
42
|
+
코드베이스에서 DuckDB는 **분석 전용** 기능에만 사용:
|
|
43
|
+
- `SyncWorker`: SQLite → DuckDB 데이터 동기화 (30초 간격)
|
|
44
|
+
- `DuckDBAnalyticsStore`: 대시보드 통계 쿼리 (집계, 시계열)
|
|
45
|
+
- 핵심 기능(이벤트 저장, 임베딩, 검색)은 모두 SQLite + LanceDB 사용
|
|
46
|
+
|
|
47
|
+
## 현재 코드 상태
|
|
48
|
+
|
|
49
|
+
이미 일부 graceful degradation 코드가 존재:
|
|
50
|
+
```typescript
|
|
51
|
+
// memory-service.ts
|
|
52
|
+
if (this.analyticsStore) {
|
|
53
|
+
await this.analyticsStore.initialize();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn('Analytics store (DuckDB) initialization failed, using SQLite for reads');
|
|
56
|
+
// Continue without analytics
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
그러나 **설치 단계**에서 실패하므로 runtime graceful degradation이 무의미.
|
|
61
|
+
|
|
62
|
+
## 영향 범위
|
|
63
|
+
|
|
64
|
+
- Node.js v20, v22: 정상 설치 (pre-built binary 존재)
|
|
65
|
+
- Node.js v24+: 설치 실패 (pre-built binary 없음 + 컴파일 실패)
|
|
66
|
+
- 향후 Node.js v25+: 동일 문제 발생 예상
|
|
67
|
+
|
|
68
|
+
## 관련 파일
|
|
69
|
+
|
|
70
|
+
| 파일 | 역할 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `package.json` | `"duckdb": "^0.10.0"` — required dependency |
|
|
73
|
+
| `src/core/db-wrapper.ts` | `import duckdb from 'duckdb'` — top-level import |
|
|
74
|
+
| `src/core/sync-worker.ts` | DuckDB SyncWorker 구현 |
|
|
75
|
+
| `src/core/edge-repo.ts` | DuckDB analytics queries |
|
|
76
|
+
| `src/services/memory-service.ts` | analyticsStore 초기화 및 사용 |
|
|
77
|
+
| `src/server/api/utils.ts` | 대시보드 API에서 DuckDB 사용 |
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Plan: DuckDB 완전 제거
|
|
2
|
+
|
|
3
|
+
## 전략
|
|
4
|
+
|
|
5
|
+
`duckdb`를 완전히 제거하고 `better-sqlite3`으로 모든 기능을 통합한다.
|
|
6
|
+
`better-sqlite3`은 이미 primary dependency이므로 새 패키지 추가 없음.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 변경 파일 목록
|
|
11
|
+
|
|
12
|
+
| 파일 | 변경 |
|
|
13
|
+
|------|------|
|
|
14
|
+
| `package.json` | `duckdb` 제거 |
|
|
15
|
+
| `src/core/db-wrapper.ts` | DuckDB API → better-sqlite3 API로 교체 |
|
|
16
|
+
| `src/core/edge-repo.ts` | `Database` 타입 변경 (duckdb → better-sqlite3) |
|
|
17
|
+
| `src/services/memory-service.ts` | analyticsStore / DuckDBAnalyticsStore 관련 코드 제거, SyncWorker 제거 |
|
|
18
|
+
| `src/core/sync-worker.ts` | 파일 삭제 (또는 empty stub) |
|
|
19
|
+
| `src/core/sqlite-event-store.ts` | edges 테이블 CREATE TABLE 추가 (없으면) |
|
|
20
|
+
| `src/server/api/*.ts` | DuckDB analytics 쿼리 → SQLite 직접 쿼리로 교체 |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 1: `src/core/db-wrapper.ts` 교체
|
|
25
|
+
|
|
26
|
+
DuckDB callback API를 better-sqlite3 동기 API로 교체.
|
|
27
|
+
better-sqlite3은 동기 API이므로 Promise wrapping이 간단해짐.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// 변경 전: import duckdb from 'duckdb';
|
|
31
|
+
// 변경 후:
|
|
32
|
+
import Database from 'better-sqlite3';
|
|
33
|
+
export type { Database };
|
|
34
|
+
|
|
35
|
+
export async function dbRun(db: Database, sql: string, params: unknown[] = []): Promise<void> {
|
|
36
|
+
db.prepare(sql).run(...params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function dbAll<T>(db: Database, sql: string, params: unknown[] = []): Promise<T[]> {
|
|
40
|
+
return db.prepare(sql).all(...(params as never[])) as T[];
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`toDate()`, `convertBigInts()` 등 유틸은 그대로 유지.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 2: Edges 테이블을 SQLite에 추가
|
|
49
|
+
|
|
50
|
+
`src/core/sqlite-event-store.ts`의 `initialize()` 에서 edges 테이블 생성:
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
54
|
+
edge_id TEXT PRIMARY KEY,
|
|
55
|
+
src_type TEXT NOT NULL,
|
|
56
|
+
src_id TEXT NOT NULL,
|
|
57
|
+
rel_type TEXT NOT NULL,
|
|
58
|
+
dst_type TEXT NOT NULL,
|
|
59
|
+
dst_id TEXT NOT NULL,
|
|
60
|
+
meta_json TEXT DEFAULT '{}',
|
|
61
|
+
created_at TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
SQLite는 `WITH CTE`, `JOIN`, `GROUP BY` 등 edge-repo의 모든 쿼리를 지원함.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Step 3: `src/services/memory-service.ts` 정리
|
|
72
|
+
|
|
73
|
+
제거할 것:
|
|
74
|
+
- `DuckDBAnalyticsStore` import 및 인스턴스
|
|
75
|
+
- `SyncWorker` import 및 인스턴스
|
|
76
|
+
- `analyticsStore` 관련 초기화/종료 코드
|
|
77
|
+
- `analyticsEnabled` 옵션 (또는 no-op으로 유지)
|
|
78
|
+
|
|
79
|
+
EdgeRepo는 SQLite DB (`this.sqliteStore.db`)를 직접 사용하도록 연결.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Step 4: `src/core/sync-worker.ts` 제거
|
|
84
|
+
|
|
85
|
+
SyncWorker는 SQLite → DuckDB 동기화 전용. DuckDB 제거 시 불필요.
|
|
86
|
+
파일 삭제 또는 empty class로 대체.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Step 5: 대시보드 Analytics API
|
|
91
|
+
|
|
92
|
+
DuckDB analytics store를 사용하는 서버 API들을 확인하고
|
|
93
|
+
SQLite 직접 쿼리로 교체하거나 `analyticsEnabled: false` 분기를 기본값으로 변경.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Step 6: `package.json`에서 `duckdb` 제거
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
// 제거
|
|
101
|
+
"duckdb": "^0.10.0"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 동작 흐름 (After)
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
npm install -g claude-memory-layer
|
|
110
|
+
→ duckdb 없음 → 빠르고 안정적인 설치 ✅
|
|
111
|
+
|
|
112
|
+
메모리 저장/조회
|
|
113
|
+
→ SQLite (primary, 변경 없음) ✅
|
|
114
|
+
|
|
115
|
+
시맨틱 검색
|
|
116
|
+
→ LanceDB (변경 없음) ✅
|
|
117
|
+
|
|
118
|
+
Edges (task blockers 등)
|
|
119
|
+
→ SQLite (기존 DuckDB 쿼리 그대로, DB만 변경) ✅
|
|
120
|
+
|
|
121
|
+
대시보드 통계
|
|
122
|
+
→ SQLite 직접 쿼리 ✅
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 주의사항
|
|
128
|
+
|
|
129
|
+
- **기존 DuckDB edges 데이터 손실**: 기존 사용자의 DuckDB edges 데이터는 마이그레이션되지 않음.
|
|
130
|
+
edges는 task blocker 등 임시적 관계 데이터이므로 손실 허용 가능.
|
|
131
|
+
- **`ON CONFLICT DO NOTHING`**: SQLite와 DuckDB 모두 지원. edge-repo 쿼리 변경 불필요.
|
|
132
|
+
- **better-sqlite3 동기 API**: `dbRun`/`dbAll`을 Promise로 wrapping하면 edge-repo 코드 변경 최소화.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 검증
|
|
137
|
+
|
|
138
|
+
1. `npm run build` 성공
|
|
139
|
+
2. Node.js v24 환경에서 `npm install` 성공
|
|
140
|
+
3. edges CRUD 동작 확인 (`/api/edges` 등)
|
|
141
|
+
4. 대시보드 API 에러 없음
|
|
142
|
+
5. session-start, user-prompt-submit hooks 정상 동작
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Spec: DuckDB 완전 제거 → SQLite로 통합
|
|
2
|
+
|
|
3
|
+
## 결론
|
|
4
|
+
|
|
5
|
+
DuckDB를 **완전히 제거**하고 SQLite로 통합할 수 있다.
|
|
6
|
+
|
|
7
|
+
DuckDB가 하는 일:
|
|
8
|
+
1. **Edges 테이블** — 표준 SQL (INSERT/SELECT/WITH CTE/JOIN). SQLite가 동일하게 지원
|
|
9
|
+
2. **Analytics Store** — SQLite 데이터 복사본. 대부분 `analyticsEnabled: false`로 이미 비활성화됨
|
|
10
|
+
|
|
11
|
+
`better-sqlite3`은 이미 primary dependency이므로 새 의존성 추가 없음.
|
|
12
|
+
|
|
13
|
+
## 요구사항
|
|
14
|
+
|
|
15
|
+
### 기능 요구사항
|
|
16
|
+
|
|
17
|
+
1. **설치 성공**: Node.js v20, v22, v24+ 모든 버전에서 `npm install -g claude-memory-layer` 성공
|
|
18
|
+
2. **Edges 기능 유지**: `edge-repo.ts`의 모든 쿼리가 SQLite에서 동일하게 동작
|
|
19
|
+
3. **Analytics fallback**: DuckDB analytics store 제거 후 대시보드가 SQLite 직접 쿼리로 동작
|
|
20
|
+
4. **기존 핵심 기능 완전 보존**: 이벤트 저장, 임베딩, 벡터 검색, session/prompt hooks 변경 없음
|
|
21
|
+
5. **SyncWorker 제거**: SQLite → DuckDB sync가 불필요해지므로 삭제
|
|
22
|
+
|
|
23
|
+
### 비기능 요구사항
|
|
24
|
+
|
|
25
|
+
- `duckdb` package.json에서 완전 제거
|
|
26
|
+
- `npm run build` 및 TypeScript strict 통과
|
|
27
|
+
- 기존 SQLite DB 데이터 마이그레이션 불필요 (edges 테이블을 SQLite로 이전 — 기존 DuckDB edges 데이터는 버려도 됨)
|
|
28
|
+
|
|
29
|
+
## 성공 기준
|
|
30
|
+
|
|
31
|
+
- [ ] `package.json`에서 `duckdb` 제거됨
|
|
32
|
+
- [ ] `npm install -g claude-memory-layer` Node.js v24에서 성공
|
|
33
|
+
- [ ] `npm run build` 통과
|
|
34
|
+
- [ ] edges CRUD 기능 SQLite에서 동작
|
|
35
|
+
- [ ] 대시보드 API 에러 없이 응답
|
package/src/core/db-wrapper.ts
CHANGED
|
@@ -1,34 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* SQLite Database Wrapper
|
|
3
|
+
* Provides Promise-based interface over better-sqlite3 synchronous API
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
7
7
|
|
|
8
|
-
export type Database =
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Converts BigInt values to Number in an object
|
|
12
|
-
* DuckDB returns BigInt for COUNT(*) and other aggregate functions
|
|
13
|
-
*/
|
|
14
|
-
function convertBigInts<T>(obj: T): T {
|
|
15
|
-
if (obj === null || obj === undefined) return obj;
|
|
16
|
-
if (typeof obj === 'bigint') return Number(obj) as unknown as T;
|
|
17
|
-
if (obj instanceof Date) return obj; // Preserve Date objects
|
|
18
|
-
if (Array.isArray(obj)) return obj.map(convertBigInts) as unknown as T;
|
|
19
|
-
if (typeof obj === 'object') {
|
|
20
|
-
const result: Record<string, unknown> = {};
|
|
21
|
-
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
22
|
-
result[key] = convertBigInts(value);
|
|
23
|
-
}
|
|
24
|
-
return result as T;
|
|
25
|
-
}
|
|
26
|
-
return obj;
|
|
27
|
-
}
|
|
8
|
+
export type Database = BetterSqlite3.Database;
|
|
28
9
|
|
|
29
10
|
/**
|
|
30
11
|
* Safely converts a value to a Date object
|
|
31
|
-
* Handles both Date objects and string timestamps from DuckDB
|
|
32
12
|
*/
|
|
33
13
|
export function toDate(value: unknown): Date {
|
|
34
14
|
if (value instanceof Date) return value;
|
|
@@ -42,78 +22,43 @@ export interface DatabaseOptions {
|
|
|
42
22
|
}
|
|
43
23
|
|
|
44
24
|
/**
|
|
45
|
-
* Creates a new
|
|
25
|
+
* Creates a new SQLite database connection
|
|
46
26
|
*/
|
|
47
|
-
export function createDatabase(
|
|
48
|
-
|
|
49
|
-
return new duckdb.Database(path, { access_mode: 'READ_ONLY' });
|
|
50
|
-
}
|
|
51
|
-
return new duckdb.Database(path);
|
|
27
|
+
export function createDatabase(dbPath: string, options?: DatabaseOptions): Database {
|
|
28
|
+
return new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
52
29
|
}
|
|
53
30
|
|
|
54
31
|
/**
|
|
55
|
-
*
|
|
32
|
+
* Executes a statement that doesn't return rows
|
|
56
33
|
*/
|
|
57
34
|
export function dbRun(db: Database, sql: string, params: unknown[] = []): Promise<void> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
db.run(sql, (err: Error | null) => {
|
|
61
|
-
if (err) reject(err);
|
|
62
|
-
else resolve();
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
db.run(sql, ...params, (err: Error | null) => {
|
|
66
|
-
if (err) reject(err);
|
|
67
|
-
else resolve();
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
});
|
|
35
|
+
db.prepare(sql).run(...(params as never[]));
|
|
36
|
+
return Promise.resolve();
|
|
71
37
|
}
|
|
72
38
|
|
|
73
39
|
/**
|
|
74
|
-
*
|
|
75
|
-
* Automatically converts BigInt values to Number
|
|
40
|
+
* Executes a query and returns all rows
|
|
76
41
|
*/
|
|
77
42
|
export function dbAll<T = Record<string, unknown>>(
|
|
78
43
|
db: Database,
|
|
79
44
|
sql: string,
|
|
80
45
|
params: unknown[] = []
|
|
81
46
|
): Promise<T[]> {
|
|
82
|
-
return
|
|
83
|
-
if (params.length === 0) {
|
|
84
|
-
db.all(sql, (err: Error | null, rows: T[]) => {
|
|
85
|
-
if (err) reject(err);
|
|
86
|
-
else resolve(convertBigInts(rows || []));
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
db.all(sql, ...params, (err: Error | null, rows: T[]) => {
|
|
90
|
-
if (err) reject(err);
|
|
91
|
-
else resolve(convertBigInts(rows || []));
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
});
|
|
47
|
+
return Promise.resolve(db.prepare(sql).all(...(params as never[])) as T[]);
|
|
95
48
|
}
|
|
96
49
|
|
|
97
50
|
/**
|
|
98
|
-
*
|
|
51
|
+
* Closes the database connection
|
|
99
52
|
*/
|
|
100
53
|
export function dbClose(db: Database): Promise<void> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (err) reject(err);
|
|
104
|
-
else resolve();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
54
|
+
db.close();
|
|
55
|
+
return Promise.resolve();
|
|
107
56
|
}
|
|
108
57
|
|
|
109
58
|
/**
|
|
110
|
-
*
|
|
59
|
+
* Executes multiple statements
|
|
111
60
|
*/
|
|
112
61
|
export function dbExec(db: Database, sql: string): Promise<void> {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (err) reject(err);
|
|
116
|
-
else resolve();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
62
|
+
db.exec(sql);
|
|
63
|
+
return Promise.resolve();
|
|
119
64
|
}
|
|
@@ -531,6 +531,30 @@ export class SQLiteEventStore {
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Get session IDs that have events but no session_summary event.
|
|
536
|
+
* Used to backfill summaries for sessions that ended without Stop hook.
|
|
537
|
+
*/
|
|
538
|
+
async getSessionsWithoutSummary(currentSessionId: string, limit = 5): Promise<string[]> {
|
|
539
|
+
await this.initialize();
|
|
540
|
+
const rows = sqliteAll<{ session_id: string }>(
|
|
541
|
+
this.db,
|
|
542
|
+
`SELECT DISTINCT e.session_id
|
|
543
|
+
FROM events e
|
|
544
|
+
WHERE e.session_id != ?
|
|
545
|
+
AND e.event_type != 'session_summary'
|
|
546
|
+
AND e.session_id NOT IN (
|
|
547
|
+
SELECT DISTINCT session_id FROM events WHERE event_type = 'session_summary'
|
|
548
|
+
)
|
|
549
|
+
GROUP BY e.session_id
|
|
550
|
+
HAVING COUNT(*) >= 3
|
|
551
|
+
ORDER BY MAX(e.timestamp) DESC
|
|
552
|
+
LIMIT ?`,
|
|
553
|
+
[currentSessionId, limit]
|
|
554
|
+
);
|
|
555
|
+
return rows.map((r) => r.session_id);
|
|
556
|
+
}
|
|
557
|
+
|
|
534
558
|
/**
|
|
535
559
|
* Get events by session ID
|
|
536
560
|
*/
|
|
@@ -40,9 +40,33 @@ const ALWAYS_STORE_TOOLS = new Set([
|
|
|
40
40
|
'Write', 'Edit', 'MultiEdit', 'Agent', 'Task', 'ExitPlanMode'
|
|
41
41
|
]);
|
|
42
42
|
|
|
43
|
+
// Keywords that indicate a Bash output is worth storing
|
|
44
|
+
const IMPORTANT_BASH_KEYWORDS = [
|
|
45
|
+
'error', 'failed', 'exception', 'traceback', 'panic',
|
|
46
|
+
'warning', 'deprecated',
|
|
47
|
+
'test passed', 'test failed', 'tests passed', 'tests failed',
|
|
48
|
+
'coverage', 'assert',
|
|
49
|
+
'published', 'deployed', 'built successfully', 'build complete',
|
|
50
|
+
'successfully installed', 'successfully created',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* For Bash commands, only store output that is significant:
|
|
55
|
+
* - Has stderr content
|
|
56
|
+
* - Contains important keywords (errors, test results, deploy events)
|
|
57
|
+
* - Output is very long (> 800 chars), indicating meaningful work
|
|
58
|
+
*/
|
|
59
|
+
function isBashSignificant(output: string, response: PostToolUseInput['tool_response']): boolean {
|
|
60
|
+
if (response?.stderr && response.stderr.trim().length > 20) return true;
|
|
61
|
+
const lower = output.toLowerCase();
|
|
62
|
+
if (IMPORTANT_BASH_KEYWORDS.some((kw) => lower.includes(kw))) return true;
|
|
63
|
+
return output.trim().length > 800;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Determine if a tool output is significant enough to store.
|
|
45
68
|
* Always-store tools bypass the length check.
|
|
69
|
+
* Bash uses keyword-based significance detection.
|
|
46
70
|
* Other tools require non-empty stderr or output length >= minLen.
|
|
47
71
|
*/
|
|
48
72
|
function hasSignificantOutput(
|
|
@@ -52,6 +76,7 @@ function hasSignificantOutput(
|
|
|
52
76
|
minLen: number
|
|
53
77
|
): boolean {
|
|
54
78
|
if (ALWAYS_STORE_TOOLS.has(toolName)) return true;
|
|
79
|
+
if (toolName === 'Bash') return isBashSignificant(output, response);
|
|
55
80
|
if (response?.stderr && response.stderr.trim().length > 0) return true;
|
|
56
81
|
return output.trim().length >= minLen;
|
|
57
82
|
}
|
|
@@ -32,6 +32,10 @@ async function main(): Promise<void> {
|
|
|
32
32
|
// Start session in memory service
|
|
33
33
|
await memoryService.startSession(input.session_id, input.cwd);
|
|
34
34
|
|
|
35
|
+
// Backfill session summaries for recent sessions that ended without Stop hook
|
|
36
|
+
// (crash, force-close, etc.). Run in background - non-blocking.
|
|
37
|
+
memoryService.backfillMissingSummaries(input.session_id, 5).catch(() => {});
|
|
38
|
+
|
|
35
39
|
// Get recent context for this project (now automatically scoped)
|
|
36
40
|
const recentEvents = await memoryService.getRecentEvents(10);
|
|
37
41
|
|
package/src/hooks/stop.ts
CHANGED
|
@@ -136,6 +136,20 @@ async function main(): Promise<void> {
|
|
|
136
136
|
// Clean up turn state file after processing
|
|
137
137
|
clearTurnState(input.session_id);
|
|
138
138
|
|
|
139
|
+
// Evaluate helpfulness of retrieved memories for this session
|
|
140
|
+
try {
|
|
141
|
+
await memoryService.evaluateSessionHelpfulness(input.session_id);
|
|
142
|
+
} catch {
|
|
143
|
+
// non-critical
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generate session summary from recent events (rule-based, no LLM needed)
|
|
147
|
+
try {
|
|
148
|
+
await memoryService.generateSessionSummary(input.session_id);
|
|
149
|
+
} catch {
|
|
150
|
+
// non-critical
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
// Embeddings enqueued in SQLite - will be processed by vector worker when server runs
|
|
140
154
|
await memoryService.processPendingEmbeddings();
|
|
141
155
|
|
|
@@ -10,7 +10,6 @@ import * as crypto from 'crypto';
|
|
|
10
10
|
|
|
11
11
|
import { EventStore } from '../core/event-store.js';
|
|
12
12
|
import { SQLiteEventStore } from '../core/sqlite-event-store.js';
|
|
13
|
-
import { SyncWorker } from '../core/sync-worker.js';
|
|
14
13
|
import { VectorStore } from '../core/vector-store.js';
|
|
15
14
|
import { Embedder, getDefaultEmbedder } from '../core/embedder.js';
|
|
16
15
|
import { VectorWorker, createVectorWorker } from '../core/vector-worker.js';
|
|
@@ -182,9 +181,6 @@ export function getSessionProject(sessionId: string): SessionRegistryEntry | nul
|
|
|
182
181
|
export class MemoryService {
|
|
183
182
|
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
184
183
|
private readonly sqliteStore: SQLiteEventStore;
|
|
185
|
-
// Analytics store: DuckDB - for server reads (optional, synced from SQLite)
|
|
186
|
-
private readonly analyticsStore: EventStore | null;
|
|
187
|
-
private syncWorker: SyncWorker | null = null;
|
|
188
184
|
|
|
189
185
|
private readonly vectorStore: VectorStore;
|
|
190
186
|
private readonly embedder: Embedder;
|
|
@@ -247,32 +243,6 @@ export class MemoryService {
|
|
|
247
243
|
}
|
|
248
244
|
);
|
|
249
245
|
|
|
250
|
-
// Initialize ANALYTICS store: DuckDB (optional, for server reads)
|
|
251
|
-
// Hooks set analyticsEnabled=false to avoid DuckDB lock conflicts
|
|
252
|
-
const analyticsEnabled = config.analyticsEnabled ?? this.readOnly; // Default: enabled only for read-only (server)
|
|
253
|
-
|
|
254
|
-
if (!analyticsEnabled) {
|
|
255
|
-
// Hook mode: skip DuckDB entirely to avoid lock conflicts
|
|
256
|
-
this.analyticsStore = null;
|
|
257
|
-
} else if (this.readOnly) {
|
|
258
|
-
// Server mode: try to use DuckDB for analytics, will fallback to SQLite
|
|
259
|
-
try {
|
|
260
|
-
this.analyticsStore = new EventStore(
|
|
261
|
-
path.join(storagePath, 'analytics.duckdb'),
|
|
262
|
-
{ readOnly: true }
|
|
263
|
-
);
|
|
264
|
-
} catch {
|
|
265
|
-
// DuckDB not available, will use SQLite for reads
|
|
266
|
-
this.analyticsStore = null;
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
// Writer mode with analytics: create DuckDB for sync target
|
|
270
|
-
this.analyticsStore = new EventStore(
|
|
271
|
-
path.join(storagePath, 'analytics.duckdb'),
|
|
272
|
-
{ readOnly: false }
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
246
|
this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
|
|
277
247
|
const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
278
248
|
this.embedder = embeddingModel
|
|
@@ -306,16 +276,6 @@ export class MemoryService {
|
|
|
306
276
|
return;
|
|
307
277
|
}
|
|
308
278
|
|
|
309
|
-
// Initialize analytics store if available (DuckDB)
|
|
310
|
-
if (this.analyticsStore) {
|
|
311
|
-
try {
|
|
312
|
-
await this.analyticsStore.initialize();
|
|
313
|
-
} catch (error) {
|
|
314
|
-
console.warn('[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:', error);
|
|
315
|
-
// Continue without analytics - SQLite will be used for reads
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
279
|
await this.vectorStore.initialize();
|
|
320
280
|
await this.embedder.initialize();
|
|
321
281
|
|
|
@@ -340,15 +300,6 @@ export class MemoryService {
|
|
|
340
300
|
);
|
|
341
301
|
this.graduationWorker.start();
|
|
342
302
|
|
|
343
|
-
// Start sync worker (SQLite -> DuckDB) if analytics store is available
|
|
344
|
-
if (this.analyticsStore) {
|
|
345
|
-
this.syncWorker = new SyncWorker(
|
|
346
|
-
this.sqliteStore,
|
|
347
|
-
this.analyticsStore,
|
|
348
|
-
{ intervalMs: 30000, batchSize: 500 }
|
|
349
|
-
);
|
|
350
|
-
this.syncWorker.start();
|
|
351
|
-
}
|
|
352
303
|
}
|
|
353
304
|
|
|
354
305
|
// Load endless mode setting
|
|
@@ -602,6 +553,67 @@ export class MemoryService {
|
|
|
602
553
|
);
|
|
603
554
|
}
|
|
604
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Backfill session summaries for recent sessions that are missing them.
|
|
558
|
+
* Called from session-start hook to catch sessions that ended without Stop hook.
|
|
559
|
+
*/
|
|
560
|
+
async backfillMissingSummaries(currentSessionId: string, limit = 5): Promise<void> {
|
|
561
|
+
await this.initialize();
|
|
562
|
+
|
|
563
|
+
// Get recent sessions that don't have a summary event
|
|
564
|
+
const recentSessionIds = await this.sqliteStore.getSessionsWithoutSummary(currentSessionId, limit);
|
|
565
|
+
for (const sid of recentSessionIds) {
|
|
566
|
+
try {
|
|
567
|
+
await this.generateSessionSummary(sid);
|
|
568
|
+
} catch {
|
|
569
|
+
// non-critical
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Generate a rule-based session summary from stored events.
|
|
576
|
+
* Called at session end (Stop hook) when no LLM-generated summary exists.
|
|
577
|
+
* Skips if a summary already exists for this session.
|
|
578
|
+
*/
|
|
579
|
+
async generateSessionSummary(sessionId: string): Promise<void> {
|
|
580
|
+
await this.initialize();
|
|
581
|
+
|
|
582
|
+
const events = await this.sqliteStore.getSessionEvents(sessionId);
|
|
583
|
+
if (events.length < 3) return; // Too short to summarize
|
|
584
|
+
|
|
585
|
+
// Skip if summary already exists
|
|
586
|
+
const hasSummary = events.some((e) => e.eventType === 'session_summary');
|
|
587
|
+
if (hasSummary) return;
|
|
588
|
+
|
|
589
|
+
const prompts = events.filter((e) => e.eventType === 'user_prompt');
|
|
590
|
+
const toolObs = events.filter((e) => e.eventType === 'tool_observation');
|
|
591
|
+
const toolNames = [...new Set(
|
|
592
|
+
toolObs.map((e) => (e.metadata as Record<string, unknown>)?.toolName as string).filter(Boolean)
|
|
593
|
+
)];
|
|
594
|
+
const errorObs = toolObs.filter((e) => {
|
|
595
|
+
const meta = e.metadata as Record<string, unknown>;
|
|
596
|
+
return meta?.exitCode !== undefined && meta.exitCode !== 0;
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const datePart = events[0].timestamp.toISOString().split('T')[0];
|
|
600
|
+
const parts: string[] = [`[${datePart}] ${prompts.length}턴 세션.`];
|
|
601
|
+
|
|
602
|
+
if (prompts.length > 0) {
|
|
603
|
+
const firstPrompt = prompts[0].content.slice(0, 120).replace(/\n/g, ' ');
|
|
604
|
+
parts.push(`주요 작업: ${firstPrompt}`);
|
|
605
|
+
}
|
|
606
|
+
if (toolNames.length > 0) {
|
|
607
|
+
parts.push(`사용 툴: ${toolNames.slice(0, 6).join(', ')}`);
|
|
608
|
+
}
|
|
609
|
+
if (errorObs.length > 0) {
|
|
610
|
+
parts.push(`오류 ${errorObs.length}건 발생`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const summary = parts.join('. ');
|
|
614
|
+
await this.storeSessionSummary(sessionId, summary, { generated: 'rule-based', eventCount: events.length });
|
|
615
|
+
}
|
|
616
|
+
|
|
605
617
|
/**
|
|
606
618
|
* Store a tool observation
|
|
607
619
|
*/
|
|
@@ -1275,6 +1287,7 @@ export class MemoryService {
|
|
|
1275
1287
|
await this.initialize();
|
|
1276
1288
|
await this.sqliteStore.recordRetrievalTrace({
|
|
1277
1289
|
...input,
|
|
1290
|
+
projectHash: this.projectHash || undefined,
|
|
1278
1291
|
candidateDetails: [],
|
|
1279
1292
|
selectedDetails: [],
|
|
1280
1293
|
fallbackTrace: [],
|
|
@@ -1652,11 +1665,6 @@ export class MemoryService {
|
|
|
1652
1665
|
this.vectorWorker.stop();
|
|
1653
1666
|
}
|
|
1654
1667
|
|
|
1655
|
-
// Stop sync worker
|
|
1656
|
-
if (this.syncWorker) {
|
|
1657
|
-
this.syncWorker.stop();
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
1668
|
// Close shared store
|
|
1661
1669
|
if (this.sharedEventStore) {
|
|
1662
1670
|
await this.sharedEventStore.close();
|
|
@@ -1665,10 +1673,6 @@ export class MemoryService {
|
|
|
1665
1673
|
// Close primary store (SQLite)
|
|
1666
1674
|
await this.sqliteStore.close();
|
|
1667
1675
|
|
|
1668
|
-
// Close analytics store (DuckDB)
|
|
1669
|
-
if (this.analyticsStore) {
|
|
1670
|
-
await this.analyticsStore.close();
|
|
1671
|
-
}
|
|
1672
1676
|
}
|
|
1673
1677
|
|
|
1674
1678
|
/**
|