evo-skills 0.1.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 (188) hide show
  1. package/.eslintrc.json +36 -0
  2. package/.prettierrc +11 -0
  3. package/README.md +227 -0
  4. package/USER-GUIDE.md +382 -0
  5. package/dist/cli/commands/diff.d.ts +7 -0
  6. package/dist/cli/commands/diff.d.ts.map +1 -0
  7. package/dist/cli/commands/diff.js +126 -0
  8. package/dist/cli/commands/diff.js.map +1 -0
  9. package/dist/cli/commands/freeze.d.ts +12 -0
  10. package/dist/cli/commands/freeze.d.ts.map +1 -0
  11. package/dist/cli/commands/freeze.js +93 -0
  12. package/dist/cli/commands/freeze.js.map +1 -0
  13. package/dist/cli/commands/log.d.ts +7 -0
  14. package/dist/cli/commands/log.d.ts.map +1 -0
  15. package/dist/cli/commands/log.js +76 -0
  16. package/dist/cli/commands/log.js.map +1 -0
  17. package/dist/cli/commands/rollback.d.ts +7 -0
  18. package/dist/cli/commands/rollback.d.ts.map +1 -0
  19. package/dist/cli/commands/rollback.js +92 -0
  20. package/dist/cli/commands/rollback.js.map +1 -0
  21. package/dist/cli/commands/status.d.ts +7 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +88 -0
  24. package/dist/cli/commands/status.js.map +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +25 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/config/defaults.d.ts +6 -0
  30. package/dist/config/defaults.d.ts.map +1 -0
  31. package/dist/config/defaults.js +31 -0
  32. package/dist/config/defaults.js.map +1 -0
  33. package/dist/config/index.d.ts +68 -0
  34. package/dist/config/index.d.ts.map +1 -0
  35. package/dist/config/index.js +195 -0
  36. package/dist/config/index.js.map +1 -0
  37. package/dist/core/evaluator/base-rule.d.ts +50 -0
  38. package/dist/core/evaluator/base-rule.d.ts.map +1 -0
  39. package/dist/core/evaluator/base-rule.js +69 -0
  40. package/dist/core/evaluator/base-rule.js.map +1 -0
  41. package/dist/core/evaluator/index.d.ts +33 -0
  42. package/dist/core/evaluator/index.d.ts.map +1 -0
  43. package/dist/core/evaluator/index.js +74 -0
  44. package/dist/core/evaluator/index.js.map +1 -0
  45. package/dist/core/evaluator/rules/repeated-drift.d.ts +19 -0
  46. package/dist/core/evaluator/rules/repeated-drift.d.ts.map +1 -0
  47. package/dist/core/evaluator/rules/repeated-drift.js +82 -0
  48. package/dist/core/evaluator/rules/repeated-drift.js.map +1 -0
  49. package/dist/core/evaluator/rules/repeated-manual-fix.d.ts +23 -0
  50. package/dist/core/evaluator/rules/repeated-manual-fix.d.ts.map +1 -0
  51. package/dist/core/evaluator/rules/repeated-manual-fix.js +97 -0
  52. package/dist/core/evaluator/rules/repeated-manual-fix.js.map +1 -0
  53. package/dist/core/journal/index.d.ts +76 -0
  54. package/dist/core/journal/index.d.ts.map +1 -0
  55. package/dist/core/journal/index.js +207 -0
  56. package/dist/core/journal/index.js.map +1 -0
  57. package/dist/core/observer/base-observer.d.ts +64 -0
  58. package/dist/core/observer/base-observer.d.ts.map +1 -0
  59. package/dist/core/observer/base-observer.js +142 -0
  60. package/dist/core/observer/base-observer.js.map +1 -0
  61. package/dist/core/observer/codex-observer.d.ts +71 -0
  62. package/dist/core/observer/codex-observer.d.ts.map +1 -0
  63. package/dist/core/observer/codex-observer.js +248 -0
  64. package/dist/core/observer/codex-observer.js.map +1 -0
  65. package/dist/core/observer/index.d.ts +7 -0
  66. package/dist/core/observer/index.d.ts.map +1 -0
  67. package/dist/core/observer/index.js +7 -0
  68. package/dist/core/observer/index.js.map +1 -0
  69. package/dist/core/observer/trace-manager.d.ts +82 -0
  70. package/dist/core/observer/trace-manager.d.ts.map +1 -0
  71. package/dist/core/observer/trace-manager.js +196 -0
  72. package/dist/core/observer/trace-manager.js.map +1 -0
  73. package/dist/core/origin-registry/index.d.ts +59 -0
  74. package/dist/core/origin-registry/index.d.ts.map +1 -0
  75. package/dist/core/origin-registry/index.js +235 -0
  76. package/dist/core/origin-registry/index.js.map +1 -0
  77. package/dist/core/patch-generator/base-strategy.d.ts +35 -0
  78. package/dist/core/patch-generator/base-strategy.d.ts.map +1 -0
  79. package/dist/core/patch-generator/base-strategy.js +55 -0
  80. package/dist/core/patch-generator/base-strategy.js.map +1 -0
  81. package/dist/core/patch-generator/index.d.ts +32 -0
  82. package/dist/core/patch-generator/index.d.ts.map +1 -0
  83. package/dist/core/patch-generator/index.js +101 -0
  84. package/dist/core/patch-generator/index.js.map +1 -0
  85. package/dist/core/patch-generator/strategies/add-fallback.d.ts +11 -0
  86. package/dist/core/patch-generator/strategies/add-fallback.d.ts.map +1 -0
  87. package/dist/core/patch-generator/strategies/add-fallback.js +57 -0
  88. package/dist/core/patch-generator/strategies/add-fallback.js.map +1 -0
  89. package/dist/core/patch-generator/strategies/prune-noise.d.ts +11 -0
  90. package/dist/core/patch-generator/strategies/prune-noise.d.ts.map +1 -0
  91. package/dist/core/patch-generator/strategies/prune-noise.js +48 -0
  92. package/dist/core/patch-generator/strategies/prune-noise.js.map +1 -0
  93. package/dist/core/shadow-manager/index.d.ts +65 -0
  94. package/dist/core/shadow-manager/index.d.ts.map +1 -0
  95. package/dist/core/shadow-manager/index.js +229 -0
  96. package/dist/core/shadow-manager/index.js.map +1 -0
  97. package/dist/core/shadow-registry/index.d.ts +68 -0
  98. package/dist/core/shadow-registry/index.d.ts.map +1 -0
  99. package/dist/core/shadow-registry/index.js +173 -0
  100. package/dist/core/shadow-registry/index.js.map +1 -0
  101. package/dist/daemon/index.d.ts +43 -0
  102. package/dist/daemon/index.d.ts.map +1 -0
  103. package/dist/daemon/index.js +144 -0
  104. package/dist/daemon/index.js.map +1 -0
  105. package/dist/storage/index.d.ts +7 -0
  106. package/dist/storage/index.d.ts.map +1 -0
  107. package/dist/storage/index.js +7 -0
  108. package/dist/storage/index.js.map +1 -0
  109. package/dist/storage/markdown.d.ts +66 -0
  110. package/dist/storage/markdown.d.ts.map +1 -0
  111. package/dist/storage/markdown.js +227 -0
  112. package/dist/storage/markdown.js.map +1 -0
  113. package/dist/storage/ndjson.d.ts +118 -0
  114. package/dist/storage/ndjson.d.ts.map +1 -0
  115. package/dist/storage/ndjson.js +238 -0
  116. package/dist/storage/ndjson.js.map +1 -0
  117. package/dist/storage/sqlite.d.ts +131 -0
  118. package/dist/storage/sqlite.d.ts.map +1 -0
  119. package/dist/storage/sqlite.js +415 -0
  120. package/dist/storage/sqlite.js.map +1 -0
  121. package/dist/types/index.d.ts +158 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +5 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/utils/diff.d.ts +20 -0
  126. package/dist/utils/diff.d.ts.map +1 -0
  127. package/dist/utils/diff.js +47 -0
  128. package/dist/utils/diff.js.map +1 -0
  129. package/dist/utils/hash.d.ts +17 -0
  130. package/dist/utils/hash.d.ts.map +1 -0
  131. package/dist/utils/hash.js +28 -0
  132. package/dist/utils/hash.js.map +1 -0
  133. package/dist/utils/index.d.ts +8 -0
  134. package/dist/utils/index.d.ts.map +1 -0
  135. package/dist/utils/index.js +8 -0
  136. package/dist/utils/index.js.map +1 -0
  137. package/dist/utils/logger.d.ts +11 -0
  138. package/dist/utils/logger.d.ts.map +1 -0
  139. package/dist/utils/logger.js +64 -0
  140. package/dist/utils/logger.js.map +1 -0
  141. package/dist/utils/path.d.ts +67 -0
  142. package/dist/utils/path.d.ts.map +1 -0
  143. package/dist/utils/path.js +115 -0
  144. package/dist/utils/path.js.map +1 -0
  145. package/docs/ENGINEERING_PLAN.md +1065 -0
  146. package/docs/PRD.md +1004 -0
  147. package/docs/PROGRESS.md +523 -0
  148. package/package.json +60 -0
  149. package/src/cli/commands/diff.ts +158 -0
  150. package/src/cli/commands/freeze.ts +108 -0
  151. package/src/cli/commands/log.ts +87 -0
  152. package/src/cli/commands/rollback.ts +99 -0
  153. package/src/cli/commands/status.ts +101 -0
  154. package/src/cli/index.ts +31 -0
  155. package/src/config/defaults.ts +32 -0
  156. package/src/config/index.ts +220 -0
  157. package/src/core/evaluator/base-rule.ts +90 -0
  158. package/src/core/evaluator/index.ts +86 -0
  159. package/src/core/evaluator/rules/repeated-drift.ts +109 -0
  160. package/src/core/evaluator/rules/repeated-manual-fix.ts +129 -0
  161. package/src/core/journal/index.ts +286 -0
  162. package/src/core/observer/base-observer.ts +200 -0
  163. package/src/core/observer/codex-observer.ts +309 -0
  164. package/src/core/observer/index.ts +7 -0
  165. package/src/core/observer/trace-manager.ts +250 -0
  166. package/src/core/origin-registry/index.ts +264 -0
  167. package/src/core/patch-generator/base-strategy.ts +67 -0
  168. package/src/core/patch-generator/index.ts +118 -0
  169. package/src/core/patch-generator/strategies/add-fallback.ts +69 -0
  170. package/src/core/patch-generator/strategies/prune-noise.ts +65 -0
  171. package/src/core/shadow-manager/index.ts +278 -0
  172. package/src/core/shadow-registry/index.ts +200 -0
  173. package/src/daemon/index.ts +174 -0
  174. package/src/storage/index.ts +7 -0
  175. package/src/storage/markdown.ts +267 -0
  176. package/src/storage/ndjson.ts +273 -0
  177. package/src/storage/sqlite.ts +555 -0
  178. package/src/types/index.ts +212 -0
  179. package/src/utils/diff.ts +60 -0
  180. package/src/utils/hash.ts +31 -0
  181. package/src/utils/index.ts +8 -0
  182. package/src/utils/logger.ts +78 -0
  183. package/src/utils/path.ts +129 -0
  184. package/tests/unit/config.test.ts +65 -0
  185. package/tests/unit/path.test.ts +89 -0
  186. package/tests/unit/utils.test.ts +45 -0
  187. package/tsconfig.json +31 -0
  188. package/vitest.config.ts +19 -0
@@ -0,0 +1,309 @@
1
+ import { watch, type FSWatcher } from 'chokidar';
2
+ import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { join, basename } from 'node:path';
4
+ import { createInterface } from 'node:readline';
5
+ import { BaseObserver } from './base-observer.js';
6
+ import { createChildLogger } from '../../utils/logger.js';
7
+
8
+ const logger = createChildLogger('codex-observer');
9
+
10
+ /**
11
+ * Codex Observer
12
+ * 监听 Codex 的 JSONL 事件流和 session 日志
13
+ */
14
+ export class CodexObserver extends BaseObserver {
15
+ private watcher: FSWatcher | null = null;
16
+ private sessionDir: string;
17
+ private currentSessionId: string | null = null;
18
+ private turnCounter: number = 0;
19
+ private lastFileSize: Map<string, number> = new Map();
20
+
21
+ constructor(sessionDir?: string) {
22
+ super('codex');
23
+ this.sessionDir = sessionDir ?? this.getDefaultSessionDir();
24
+ }
25
+
26
+ /**
27
+ * 获取默认 session 目录
28
+ */
29
+ private getDefaultSessionDir(): string {
30
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
31
+ return join(home, '.codex', 'sessions');
32
+ }
33
+
34
+ /**
35
+ * 启动 observer
36
+ */
37
+ async start(): Promise<void> {
38
+ if (this.isRunning) {
39
+ logger.warn('Observer already running');
40
+ return;
41
+ }
42
+
43
+ if (!existsSync(this.sessionDir)) {
44
+ logger.warn(`Session directory not found: ${this.sessionDir}`);
45
+ return;
46
+ }
47
+
48
+ this.isRunning = true;
49
+ logger.info('Starting Codex observer', { sessionDir: this.sessionDir });
50
+
51
+ // 监听 session 目录的变化
52
+ this.watcher = watch(this.sessionDir, {
53
+ persistent: true,
54
+ ignoreInitial: false,
55
+ awaitWriteFinish: {
56
+ stabilityThreshold: 100,
57
+ pollInterval: 50,
58
+ },
59
+ });
60
+
61
+ this.watcher.on('add', (path) => this.handleFileAdd(path));
62
+ this.watcher.on('change', (path) => this.handleFileChange(path));
63
+
64
+ logger.info('Codex observer started');
65
+ }
66
+
67
+ /**
68
+ * 停止 observer
69
+ */
70
+ async stop(): Promise<void> {
71
+ if (!this.isRunning) {
72
+ return;
73
+ }
74
+
75
+ this.isRunning = false;
76
+
77
+ if (this.watcher) {
78
+ await this.watcher.close();
79
+ this.watcher = null;
80
+ }
81
+
82
+ logger.info('Codex observer stopped');
83
+ }
84
+
85
+ /**
86
+ * 处理新文件
87
+ */
88
+ private handleFileAdd(path: string): void {
89
+ if (!path.endsWith('.jsonl')) {
90
+ return;
91
+ }
92
+
93
+ const sessionId = this.extractSessionId(path);
94
+ this.currentSessionId = sessionId;
95
+ this.lastFileSize.set(path, 0);
96
+
97
+ logger.info(`New session detected: ${sessionId}`, { path });
98
+
99
+ // 读取整个文件
100
+ this.processJsonlFile(path);
101
+ }
102
+
103
+ /**
104
+ * 处理文件变化
105
+ */
106
+ private handleFileChange(path: string): void {
107
+ if (!path.endsWith('.jsonl')) {
108
+ return;
109
+ }
110
+
111
+ const lastSize = this.lastFileSize.get(path) ?? 0;
112
+ const currentSize = this.getFileSize(path);
113
+
114
+ if (currentSize > lastSize) {
115
+ // 只读取新增的部分
116
+ this.processJsonlFileIncremental(path, lastSize);
117
+ this.lastFileSize.set(path, currentSize);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 处理 JSONL 文件(全量)
123
+ */
124
+ private processJsonlFile(path: string): void {
125
+ const sessionId = this.extractSessionId(path);
126
+
127
+ try {
128
+ const content = readFileSync(path, 'utf-8');
129
+ const lines = content.split('\n').filter((line) => line.trim());
130
+
131
+ for (const line of lines) {
132
+ try {
133
+ const event = JSON.parse(line);
134
+ this.processEvent(sessionId, event);
135
+ } catch {
136
+ // 忽略解析错误
137
+ }
138
+ }
139
+ } catch (error) {
140
+ logger.warn(`Failed to read JSONL file: ${path}`, { error });
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 处理 JSONL 文件(增量)
146
+ */
147
+ private processJsonlFileIncremental(path: string, startOffset: number): void {
148
+ const sessionId = this.extractSessionId(path);
149
+
150
+ try {
151
+ const stream = createReadStream(path, {
152
+ start: startOffset,
153
+ encoding: 'utf-8',
154
+ });
155
+
156
+ const rl = createInterface({
157
+ input: stream,
158
+ crlfDelay: Infinity,
159
+ });
160
+
161
+ rl.on('line', (line) => {
162
+ if (line.trim()) {
163
+ try {
164
+ const event = JSON.parse(line);
165
+ this.processEvent(sessionId, event);
166
+ } catch {
167
+ // 忽略解析错误
168
+ }
169
+ }
170
+ });
171
+ } catch (error) {
172
+ logger.warn(`Failed to read JSONL file incrementally: ${path}`, { error });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 处理单个事件
178
+ */
179
+ private processEvent(sessionId: string, event: Record<string, unknown>): void {
180
+ const turnId = `turn_${++this.turnCounter}`;
181
+
182
+ // 根据事件类型创建相应的 trace
183
+ switch (event.type) {
184
+ case 'user_input':
185
+ this.emitTrace(
186
+ this.createUserInputTrace(sessionId, turnId, event.content as string)
187
+ );
188
+ break;
189
+
190
+ case 'assistant_output':
191
+ case 'assistant_message':
192
+ this.emitTrace(
193
+ this.createAssistantOutputTrace(sessionId, turnId, event.content as string)
194
+ );
195
+ break;
196
+
197
+ case 'tool_call':
198
+ this.emitTrace(
199
+ this.createToolCallTrace(sessionId, turnId, event.tool as string, (event.args as Record<string, unknown>) ?? {})
200
+ );
201
+ break;
202
+
203
+ case 'tool_result':
204
+ this.emitTrace(
205
+ this.createToolResultTrace(
206
+ sessionId,
207
+ turnId,
208
+ event.tool as string,
209
+ (event.result as Record<string, unknown>) ?? {},
210
+ event.status === 'error' ? 'failure' : 'success'
211
+ )
212
+ );
213
+ break;
214
+
215
+ case 'file_change':
216
+ case 'file_write':
217
+ case 'file_edit':
218
+ if (event.files) {
219
+ this.emitTrace(
220
+ this.createFileChangeTrace(sessionId, turnId, event.files as string[])
221
+ );
222
+ }
223
+ break;
224
+
225
+ case 'retry':
226
+ this.emitTrace(
227
+ this.createRetryTrace(sessionId, turnId, event.reason as string)
228
+ );
229
+ break;
230
+
231
+ case 'error':
232
+ this.emitTrace(
233
+ this.createStatusTrace(sessionId, turnId, 'failure', {
234
+ error: event.error,
235
+ })
236
+ );
237
+ break;
238
+
239
+ case 'session_start':
240
+ logger.info(`Session started: ${sessionId}`);
241
+ break;
242
+
243
+ case 'session_end':
244
+ logger.info(`Session ended: ${sessionId}`);
245
+ break;
246
+
247
+ default:
248
+ // 忽略未知事件类型
249
+ break;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 从文件路径提取 session ID
255
+ */
256
+ private extractSessionId(path: string): string {
257
+ const filename = basename(path, '.jsonl');
258
+ return filename;
259
+ }
260
+
261
+ /**
262
+ * 获取文件大小
263
+ */
264
+ private getFileSize(path: string): number {
265
+ try {
266
+ return statSync(path).size;
267
+ } catch {
268
+ return 0;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * 手动处理一个 session 文件
274
+ */
275
+ async processSessionFile(filePath: string): Promise<void> {
276
+ if (!existsSync(filePath)) {
277
+ throw new Error(`Session file not found: ${filePath}`);
278
+ }
279
+
280
+ logger.info(`Processing session file: ${filePath}`);
281
+ this.processJsonlFile(filePath);
282
+ }
283
+
284
+ /**
285
+ * 获取当前 session ID
286
+ */
287
+ getCurrentSessionId(): string | null {
288
+ return this.currentSessionId;
289
+ }
290
+
291
+ /**
292
+ * 设置 session 目录
293
+ */
294
+ setSessionDir(dir: string): void {
295
+ this.sessionDir = dir;
296
+ }
297
+
298
+ /**
299
+ * 获取 session 目录
300
+ */
301
+ getSessionDir(): string {
302
+ return this.sessionDir;
303
+ }
304
+ }
305
+
306
+ // 导出工厂函数
307
+ export function createCodexObserver(sessionDir?: string): CodexObserver {
308
+ return new CodexObserver(sessionDir);
309
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Observer 模块入口
3
+ */
4
+
5
+ export { BaseObserver } from './base-observer.js';
6
+ export { CodexObserver, createCodexObserver } from './codex-observer.js';
7
+ export { TraceManager, createTraceManager } from './trace-manager.js';
@@ -0,0 +1,250 @@
1
+ import { join } from 'node:path';
2
+ import { createChildLogger } from '../../utils/logger.js';
3
+ import { createTraceStore } from '../../storage/ndjson.js';
4
+ import { createSQLiteStorage } from '../../storage/sqlite.js';
5
+ import type { Trace, RuntimeType } from '../../types/index.js';
6
+
7
+ const logger = createChildLogger('trace-manager');
8
+
9
+ /**
10
+ * Trace 存储管理器
11
+ * 负责存储和查询 trace 数据
12
+ */
13
+ export class TraceManager {
14
+ private projectRoot: string;
15
+ private db;
16
+ private traceStore;
17
+ private currentSessionId: string | null = null;
18
+
19
+ constructor(projectRoot: string) {
20
+ this.projectRoot = projectRoot;
21
+
22
+ // 初始化数据库
23
+ const dbPath = join(projectRoot, '.evo', 'state', 'sessions.db');
24
+ this.db = createSQLiteStorage(dbPath);
25
+
26
+ // 初始化 NDJSON trace store
27
+ const tracesDir = join(projectRoot, '.evo', 'state');
28
+ this.traceStore = createTraceStore(tracesDir, 'default');
29
+ }
30
+
31
+ /**
32
+ * 初始化
33
+ */
34
+ async init(): Promise<void> {
35
+ await this.db.init();
36
+ logger.info('Trace manager initialized');
37
+ }
38
+
39
+ /**
40
+ * 设置当前 session
41
+ */
42
+ setSession(sessionId: string, runtime: RuntimeType, projectId?: string): void {
43
+ this.currentSessionId = sessionId;
44
+
45
+ // 创建 session 记录
46
+ this.db.createSession({
47
+ session_id: sessionId,
48
+ runtime,
49
+ project_id: projectId ?? null,
50
+ started_at: new Date().toISOString(),
51
+ ended_at: null,
52
+ trace_count: 0,
53
+ });
54
+
55
+ // 更新 trace store 的 session ID
56
+ const tracesDir = join(this.projectRoot, '.evo', 'state');
57
+ this.traceStore = createTraceStore(tracesDir, sessionId);
58
+
59
+ logger.info(`Session set: ${sessionId}`, { runtime, projectId });
60
+ }
61
+
62
+ /**
63
+ * 记录 trace
64
+ */
65
+ recordTrace(trace: Trace): void {
66
+ // 保存到 NDJSON
67
+ this.traceStore.append(trace);
68
+
69
+ // 添加索引到数据库
70
+ this.db.addTraceIndex({
71
+ trace_id: trace.trace_id,
72
+ session_id: trace.session_id,
73
+ runtime: trace.runtime,
74
+ event_type: trace.event_type,
75
+ timestamp: trace.timestamp,
76
+ status: trace.status,
77
+ });
78
+
79
+ // 更新 session trace 计数
80
+ this.db.incrementSessionTraceCount(trace.session_id);
81
+
82
+ logger.debug('Trace recorded', {
83
+ trace_id: trace.trace_id,
84
+ event_type: trace.event_type,
85
+ });
86
+ }
87
+
88
+ /**
89
+ * 批量记录 traces
90
+ */
91
+ recordTraces(traces: Trace[]): void {
92
+ for (const trace of traces) {
93
+ this.recordTrace(trace);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 获取 session 的 traces
99
+ */
100
+ async getSessionTraces(sessionId: string, limit?: number): Promise<Trace[]> {
101
+ const allTraces = await this.traceStore.readAll();
102
+
103
+ let filtered = allTraces.filter((t) => t.session_id === sessionId);
104
+
105
+ // 按时间戳排序
106
+ filtered.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
107
+
108
+ if (limit) {
109
+ filtered = filtered.slice(0, limit);
110
+ }
111
+
112
+ return filtered;
113
+ }
114
+
115
+ /**
116
+ * 获取最近的 traces
117
+ */
118
+ async getRecentTraces(count: number): Promise<Trace[]> {
119
+ return this.traceStore.readRecent(count);
120
+ }
121
+
122
+ /**
123
+ * 按事件类型查询 traces
124
+ */
125
+ async getTracesByEventType(
126
+ sessionId: string,
127
+ eventType: string
128
+ ): Promise<Trace[]> {
129
+ const allTraces = await this.traceStore.readAll();
130
+ return allTraces.filter(
131
+ (t) => t.session_id === sessionId && t.event_type === eventType
132
+ );
133
+ }
134
+
135
+ /**
136
+ * 按时间范围查询 traces
137
+ */
138
+ async getTracesByTimeRange(
139
+ sessionId: string,
140
+ startTime: string,
141
+ endTime: string
142
+ ): Promise<Trace[]> {
143
+ const allTraces = await this.traceStore.readAll();
144
+ const start = new Date(startTime).getTime();
145
+ const end = new Date(endTime).getTime();
146
+
147
+ return allTraces.filter((t) => {
148
+ if (t.session_id !== sessionId) return false;
149
+ const time = new Date(t.timestamp).getTime();
150
+ return time >= start && time <= end;
151
+ });
152
+ }
153
+
154
+ /**
155
+ * 获取失败的 traces
156
+ */
157
+ async getFailedTraces(sessionId: string): Promise<Trace[]> {
158
+ const allTraces = await this.traceStore.readAll();
159
+ return allTraces.filter(
160
+ (t) => t.session_id === sessionId && t.status === 'failure'
161
+ );
162
+ }
163
+
164
+ /**
165
+ * 获取重试的 traces
166
+ */
167
+ async getRetryTraces(sessionId: string): Promise<Trace[]> {
168
+ const allTraces = await this.traceStore.readAll();
169
+ return allTraces.filter(
170
+ (t) => t.session_id === sessionId && t.event_type === 'retry'
171
+ );
172
+ }
173
+
174
+ /**
175
+ * 获取文件变化的 traces
176
+ */
177
+ async getFileChangeTraces(sessionId: string): Promise<Trace[]> {
178
+ const allTraces = await this.traceStore.readAll();
179
+ return allTraces.filter(
180
+ (t) => t.session_id === sessionId && t.event_type === 'file_change'
181
+ );
182
+ }
183
+
184
+ /**
185
+ * 获取 trace 统计信息
186
+ */
187
+ async getTraceStats(sessionId: string): Promise<{
188
+ total: number;
189
+ byEventType: Record<string, number>;
190
+ byStatus: Record<string, number>;
191
+ }> {
192
+ const allTraces = await this.traceStore.readAll();
193
+ const sessionTraces = allTraces.filter((t) => t.session_id === sessionId);
194
+
195
+ const byEventType: Record<string, number> = {};
196
+ const byStatus: Record<string, number> = {};
197
+
198
+ for (const trace of sessionTraces) {
199
+ byEventType[trace.event_type] = (byEventType[trace.event_type] ?? 0) + 1;
200
+ byStatus[trace.status] = (byStatus[trace.status] ?? 0) + 1;
201
+ }
202
+
203
+ return {
204
+ total: sessionTraces.length,
205
+ byEventType,
206
+ byStatus,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * 清理旧的 traces
212
+ */
213
+ async cleanupOldTraces(retentionDays: number): Promise<number> {
214
+ // 这里可以实现清理逻辑
215
+ // 暂时返回 0
216
+ logger.info(`Cleanup old traces older than ${retentionDays} days`);
217
+ return 0;
218
+ }
219
+
220
+ /**
221
+ * 获取当前 session ID
222
+ */
223
+ getCurrentSessionId(): string | null {
224
+ return this.currentSessionId;
225
+ }
226
+
227
+ /**
228
+ * 结束当前 session
229
+ */
230
+ endSession(): void {
231
+ if (this.currentSessionId) {
232
+ logger.info(`Session ended: ${this.currentSessionId}`);
233
+ this.currentSessionId = null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * 关闭
239
+ */
240
+ close(): void {
241
+ this.endSession();
242
+ this.db.close();
243
+ logger.info('Trace manager closed');
244
+ }
245
+ }
246
+
247
+ // 导出工厂函数
248
+ export function createTraceManager(projectRoot: string): TraceManager {
249
+ return new TraceManager(projectRoot);
250
+ }