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.
- package/.eslintrc.json +36 -0
- package/.prettierrc +11 -0
- package/README.md +227 -0
- package/USER-GUIDE.md +382 -0
- package/dist/cli/commands/diff.d.ts +7 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +126 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/freeze.d.ts +12 -0
- package/dist/cli/commands/freeze.d.ts.map +1 -0
- package/dist/cli/commands/freeze.js +93 -0
- package/dist/cli/commands/freeze.js.map +1 -0
- package/dist/cli/commands/log.d.ts +7 -0
- package/dist/cli/commands/log.d.ts.map +1 -0
- package/dist/cli/commands/log.js +76 -0
- package/dist/cli/commands/log.js.map +1 -0
- package/dist/cli/commands/rollback.d.ts +7 -0
- package/dist/cli/commands/rollback.d.ts.map +1 -0
- package/dist/cli/commands/rollback.js +92 -0
- package/dist/cli/commands/rollback.js.map +1 -0
- package/dist/cli/commands/status.d.ts +7 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +88 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +31 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +68 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +195 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/evaluator/base-rule.d.ts +50 -0
- package/dist/core/evaluator/base-rule.d.ts.map +1 -0
- package/dist/core/evaluator/base-rule.js +69 -0
- package/dist/core/evaluator/base-rule.js.map +1 -0
- package/dist/core/evaluator/index.d.ts +33 -0
- package/dist/core/evaluator/index.d.ts.map +1 -0
- package/dist/core/evaluator/index.js +74 -0
- package/dist/core/evaluator/index.js.map +1 -0
- package/dist/core/evaluator/rules/repeated-drift.d.ts +19 -0
- package/dist/core/evaluator/rules/repeated-drift.d.ts.map +1 -0
- package/dist/core/evaluator/rules/repeated-drift.js +82 -0
- package/dist/core/evaluator/rules/repeated-drift.js.map +1 -0
- package/dist/core/evaluator/rules/repeated-manual-fix.d.ts +23 -0
- package/dist/core/evaluator/rules/repeated-manual-fix.d.ts.map +1 -0
- package/dist/core/evaluator/rules/repeated-manual-fix.js +97 -0
- package/dist/core/evaluator/rules/repeated-manual-fix.js.map +1 -0
- package/dist/core/journal/index.d.ts +76 -0
- package/dist/core/journal/index.d.ts.map +1 -0
- package/dist/core/journal/index.js +207 -0
- package/dist/core/journal/index.js.map +1 -0
- package/dist/core/observer/base-observer.d.ts +64 -0
- package/dist/core/observer/base-observer.d.ts.map +1 -0
- package/dist/core/observer/base-observer.js +142 -0
- package/dist/core/observer/base-observer.js.map +1 -0
- package/dist/core/observer/codex-observer.d.ts +71 -0
- package/dist/core/observer/codex-observer.d.ts.map +1 -0
- package/dist/core/observer/codex-observer.js +248 -0
- package/dist/core/observer/codex-observer.js.map +1 -0
- package/dist/core/observer/index.d.ts +7 -0
- package/dist/core/observer/index.d.ts.map +1 -0
- package/dist/core/observer/index.js +7 -0
- package/dist/core/observer/index.js.map +1 -0
- package/dist/core/observer/trace-manager.d.ts +82 -0
- package/dist/core/observer/trace-manager.d.ts.map +1 -0
- package/dist/core/observer/trace-manager.js +196 -0
- package/dist/core/observer/trace-manager.js.map +1 -0
- package/dist/core/origin-registry/index.d.ts +59 -0
- package/dist/core/origin-registry/index.d.ts.map +1 -0
- package/dist/core/origin-registry/index.js +235 -0
- package/dist/core/origin-registry/index.js.map +1 -0
- package/dist/core/patch-generator/base-strategy.d.ts +35 -0
- package/dist/core/patch-generator/base-strategy.d.ts.map +1 -0
- package/dist/core/patch-generator/base-strategy.js +55 -0
- package/dist/core/patch-generator/base-strategy.js.map +1 -0
- package/dist/core/patch-generator/index.d.ts +32 -0
- package/dist/core/patch-generator/index.d.ts.map +1 -0
- package/dist/core/patch-generator/index.js +101 -0
- package/dist/core/patch-generator/index.js.map +1 -0
- package/dist/core/patch-generator/strategies/add-fallback.d.ts +11 -0
- package/dist/core/patch-generator/strategies/add-fallback.d.ts.map +1 -0
- package/dist/core/patch-generator/strategies/add-fallback.js +57 -0
- package/dist/core/patch-generator/strategies/add-fallback.js.map +1 -0
- package/dist/core/patch-generator/strategies/prune-noise.d.ts +11 -0
- package/dist/core/patch-generator/strategies/prune-noise.d.ts.map +1 -0
- package/dist/core/patch-generator/strategies/prune-noise.js +48 -0
- package/dist/core/patch-generator/strategies/prune-noise.js.map +1 -0
- package/dist/core/shadow-manager/index.d.ts +65 -0
- package/dist/core/shadow-manager/index.d.ts.map +1 -0
- package/dist/core/shadow-manager/index.js +229 -0
- package/dist/core/shadow-manager/index.js.map +1 -0
- package/dist/core/shadow-registry/index.d.ts +68 -0
- package/dist/core/shadow-registry/index.d.ts.map +1 -0
- package/dist/core/shadow-registry/index.js +173 -0
- package/dist/core/shadow-registry/index.js.map +1 -0
- package/dist/daemon/index.d.ts +43 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +144 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/markdown.d.ts +66 -0
- package/dist/storage/markdown.d.ts.map +1 -0
- package/dist/storage/markdown.js +227 -0
- package/dist/storage/markdown.js.map +1 -0
- package/dist/storage/ndjson.d.ts +118 -0
- package/dist/storage/ndjson.d.ts.map +1 -0
- package/dist/storage/ndjson.js +238 -0
- package/dist/storage/ndjson.js.map +1 -0
- package/dist/storage/sqlite.d.ts +131 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +415 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/types/index.d.ts +158 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/diff.d.ts +20 -0
- package/dist/utils/diff.d.ts.map +1 -0
- package/dist/utils/diff.js +47 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/hash.d.ts +17 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +28 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +64 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/path.d.ts +67 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +115 -0
- package/dist/utils/path.js.map +1 -0
- package/docs/ENGINEERING_PLAN.md +1065 -0
- package/docs/PRD.md +1004 -0
- package/docs/PROGRESS.md +523 -0
- package/package.json +60 -0
- package/src/cli/commands/diff.ts +158 -0
- package/src/cli/commands/freeze.ts +108 -0
- package/src/cli/commands/log.ts +87 -0
- package/src/cli/commands/rollback.ts +99 -0
- package/src/cli/commands/status.ts +101 -0
- package/src/cli/index.ts +31 -0
- package/src/config/defaults.ts +32 -0
- package/src/config/index.ts +220 -0
- package/src/core/evaluator/base-rule.ts +90 -0
- package/src/core/evaluator/index.ts +86 -0
- package/src/core/evaluator/rules/repeated-drift.ts +109 -0
- package/src/core/evaluator/rules/repeated-manual-fix.ts +129 -0
- package/src/core/journal/index.ts +286 -0
- package/src/core/observer/base-observer.ts +200 -0
- package/src/core/observer/codex-observer.ts +309 -0
- package/src/core/observer/index.ts +7 -0
- package/src/core/observer/trace-manager.ts +250 -0
- package/src/core/origin-registry/index.ts +264 -0
- package/src/core/patch-generator/base-strategy.ts +67 -0
- package/src/core/patch-generator/index.ts +118 -0
- package/src/core/patch-generator/strategies/add-fallback.ts +69 -0
- package/src/core/patch-generator/strategies/prune-noise.ts +65 -0
- package/src/core/shadow-manager/index.ts +278 -0
- package/src/core/shadow-registry/index.ts +200 -0
- package/src/daemon/index.ts +174 -0
- package/src/storage/index.ts +7 -0
- package/src/storage/markdown.ts +267 -0
- package/src/storage/ndjson.ts +273 -0
- package/src/storage/sqlite.ts +555 -0
- package/src/types/index.ts +212 -0
- package/src/utils/diff.ts +60 -0
- package/src/utils/hash.ts +31 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/path.ts +129 -0
- package/tests/unit/config.test.ts +65 -0
- package/tests/unit/path.test.ts +89 -0
- package/tests/unit/utils.test.ts +45 -0
- package/tsconfig.json +31 -0
- 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,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
|
+
}
|