oh-my-knowledge 0.51.2 → 0.52.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 (49) hide show
  1. package/README.md +27 -6
  2. package/README.zh.md +27 -6
  3. package/dist/assets/agent-skills/omk/SKILL.md +8 -8
  4. package/dist/observability/codex-conversation-index.d.ts +88 -0
  5. package/dist/observability/codex-conversation-index.js +566 -0
  6. package/dist/observability/codex-protocol.d.ts +12 -0
  7. package/dist/observability/codex-protocol.js +78 -0
  8. package/dist/observability/codex-tool-status.d.ts +16 -0
  9. package/dist/observability/codex-tool-status.js +114 -0
  10. package/dist/observability/codex-trace-adapter.js +342 -119
  11. package/dist/observability/conversation-catalog.d.ts +38 -0
  12. package/dist/observability/conversation-catalog.js +573 -0
  13. package/dist/observability/conversation-index-process.d.ts +1 -0
  14. package/dist/observability/conversation-index-process.js +31 -0
  15. package/dist/observability/conversation-view-model.d.ts +5 -0
  16. package/dist/observability/conversation-view-model.js +100 -0
  17. package/dist/observability/experience.d.ts +3 -1
  18. package/dist/observability/experience.js +248 -7
  19. package/dist/observability/inbox.js +40 -2
  20. package/dist/observability/knowledge-debugger.d.ts +4 -0
  21. package/dist/observability/knowledge-debugger.js +364 -0
  22. package/dist/observability/polling-subscription-hub.d.ts +27 -0
  23. package/dist/observability/polling-subscription-hub.js +149 -0
  24. package/dist/observability/source-record-archive.d.ts +13 -0
  25. package/dist/observability/source-record-archive.js +324 -0
  26. package/dist/observability/task-window.d.ts +8 -0
  27. package/dist/observability/task-window.js +36 -0
  28. package/dist/observability/trace-ir.d.ts +66 -1
  29. package/dist/observability/trace-source.d.ts +1 -0
  30. package/dist/observability/trace-source.js +12 -5
  31. package/dist/observability/turn-index.d.ts +3 -0
  32. package/dist/observability/turn-index.js +251 -0
  33. package/dist/renderer/conversation-renderer.d.ts +9 -0
  34. package/dist/renderer/conversation-renderer.js +410 -0
  35. package/dist/renderer/icons.js +1 -0
  36. package/dist/renderer/inline-markdown.d.ts +6 -0
  37. package/dist/renderer/inline-markdown.js +158 -0
  38. package/dist/renderer/knowledge-debugger-renderer.d.ts +9 -0
  39. package/dist/renderer/knowledge-debugger-renderer.js +2280 -0
  40. package/dist/renderer/observation-inbox-renderer.js +9 -0
  41. package/dist/renderer/skill-list-renderer.js +2 -1
  42. package/dist/renderer/trajectory-live.d.ts +42 -0
  43. package/dist/renderer/trajectory-live.js +258 -0
  44. package/dist/renderer/trajectory-routing.d.ts +60 -0
  45. package/dist/renderer/trajectory-routing.js +351 -0
  46. package/dist/server/report-server.d.ts +4 -1
  47. package/dist/server/report-server.js +274 -6
  48. package/dist/types/observability.d.ts +225 -1
  49. package/package.json +8 -5
@@ -0,0 +1,573 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { basename, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { DEFAULT_CACHE_DIR } from '../eval-core/default-dirs.js';
9
+ import { durationMsBetween } from '../shared/time.js';
10
+ import { writeJsonFileAtomic } from '../shared/atomic-json.js';
11
+ import { isCurrentCodexRolloutIndex, isReusableCodexRolloutIndex, normalizeCodexRolloutIndex, readCodexTaskRecords, synchronizeCurrentCodexRolloutIndex, } from './codex-conversation-index.js';
12
+ import { parseCodexSessionFile } from './codex-trace-adapter.js';
13
+ import { projectTraceSessionTimeline } from './experience.js';
14
+ import { reconstructExperienceTurns } from './turn-index.js';
15
+ import { observationSourceRecordFromLine } from './source-record-archive.js';
16
+ import { PollingSubscriptionHub, } from './polling-subscription-hub.js';
17
+ const BACKGROUND_INDEX_THRESHOLD_BYTES = 32 * 1024 * 1024;
18
+ const MAX_SOURCE_RECORD_ARCHIVE_BYTES = 16 * 1024 * 1024;
19
+ const DEFAULT_LIVE_ACTIVITY_WINDOW_MS = 5 * 60 * 1000;
20
+ export function createCodexConversationCatalog(options = {}) {
21
+ return new CodexConversationCatalog(options);
22
+ }
23
+ class CodexConversationCatalog {
24
+ codexHome;
25
+ cacheDir;
26
+ useBackgroundProcess;
27
+ backgroundProcessThresholdBytes;
28
+ liveActivityWindowMs;
29
+ now;
30
+ indexPromises = new Map();
31
+ trajectoryPromises = new Map();
32
+ liveTrajectories;
33
+ constructor(options) {
34
+ this.codexHome = options.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), '.codex');
35
+ this.cacheDir = options.cacheDir ?? join(DEFAULT_CACHE_DIR, 'conversations');
36
+ this.useBackgroundProcess = options.useBackgroundProcess ?? true;
37
+ this.backgroundProcessThresholdBytes = options.backgroundProcessThresholdBytes
38
+ ?? BACKGROUND_INDEX_THRESHOLD_BYTES;
39
+ this.liveActivityWindowMs = options.liveActivityWindowMs ?? DEFAULT_LIVE_ACTIVITY_WINDOW_MS;
40
+ this.now = options.now ?? Date.now;
41
+ this.liveTrajectories = new PollingSubscriptionHub(options.livePollIntervalMs);
42
+ }
43
+ async listConversations() {
44
+ const rows = this.listThreadRows();
45
+ const conversations = await Promise.all(rows.map((row) => this.conversationForOverview(row)));
46
+ const indexed = conversations.filter((item) => item.turnCount !== undefined);
47
+ return {
48
+ conversations,
49
+ totalTurnCount: indexed.reduce((sum, item) => sum + (item.turnCount ?? 0), 0),
50
+ totalToolCallCount: indexed.reduce((sum, item) => sum + (item.toolCallCount ?? 0), 0),
51
+ totalToolFailureCount: indexed.reduce((sum, item) => sum + (item.toolFailureCount ?? 0), 0),
52
+ indexedConversationCount: indexed.length,
53
+ unarchivedConversationCount: conversations.filter((item) => !item.archived).length,
54
+ archivedConversationCount: conversations.filter((item) => item.archived).length,
55
+ workspaceCount: new Set(conversations.flatMap((item) => item.cwd ? [item.cwd] : [])).size,
56
+ };
57
+ }
58
+ async getConversation(threadId) {
59
+ const row = this.findThreadRow(threadId);
60
+ if (!row || !existsSync(row.rolloutPath))
61
+ return undefined;
62
+ const index = await this.currentIndexFor(row);
63
+ return this.conversationFromIndex(row, index);
64
+ }
65
+ async loadTaskTrajectory(threadId, turnId) {
66
+ const key = `${threadId}\u0000${turnId}`;
67
+ const existing = this.trajectoryPromises.get(key);
68
+ if (existing)
69
+ return existing;
70
+ const pending = this.loadTaskTrajectoryUncached(threadId, turnId);
71
+ this.trajectoryPromises.set(key, pending);
72
+ try {
73
+ return await pending;
74
+ }
75
+ finally {
76
+ this.trajectoryPromises.delete(key);
77
+ }
78
+ }
79
+ async observeTaskTrajectory(threadId, turnId, observer, options = {}) {
80
+ const row = this.findThreadRow(threadId);
81
+ if (!row || !existsSync(row.rolloutPath)) {
82
+ throw new Error(`Codex 对话不存在:${threadId}`);
83
+ }
84
+ const key = `${threadId}\u0000${turnId}`;
85
+ return this.liveTrajectories.subscribe(key, async (previous) => this.loadLiveSnapshot(row, turnId, previous), {
86
+ next: ({ value }) => observer.next(value),
87
+ complete: () => observer.complete?.(),
88
+ error: (cause) => observer.error?.(cause),
89
+ }, options);
90
+ }
91
+ async loadTaskTrajectoryUncached(threadId, turnId) {
92
+ const row = this.findThreadRow(threadId);
93
+ if (!row || !existsSync(row.rolloutPath))
94
+ return undefined;
95
+ let index = await this.indexFor(row);
96
+ let indexedTask = index.tasks.find((task) => task.turnId === turnId);
97
+ if ((!indexedTask || indexedTask.status === 'open')
98
+ && !isCurrentCodexRolloutIndex(index, row.rolloutPath)) {
99
+ index = await this.awaitIndex(row);
100
+ indexedTask = index.tasks.find((task) => task.turnId === turnId);
101
+ }
102
+ if (!indexedTask)
103
+ return undefined;
104
+ return this.trajectoryFromIndex(row, index, indexedTask);
105
+ }
106
+ trajectoryFromIndex(row, index, indexedTask) {
107
+ const threadId = row.id;
108
+ const turnId = indexedTask.turnId;
109
+ const selected = readCodexTaskRecords(index, indexedTask);
110
+ const traceSession = parseCodexSessionFile(row.rolloutPath, selected.records);
111
+ const fullSessionTimeline = projectTraceSessionTimeline(traceSession);
112
+ const turns = reconstructExperienceTurns(fullSessionTimeline);
113
+ const taskTurn = turns.find((turn) => turn.sourceTurnId === turnId || turn.turnId === turnId);
114
+ const startTimestamp = taskTurn?.startTimestamp ?? indexedTask.startTimestamp;
115
+ const endTimestamp = taskTurn?.endTimestamp ?? indexedTask.endTimestamp ?? startTimestamp;
116
+ const sourceRecords = sourceRecordView(selected.lines, traceSession.traceId, row.rolloutPath);
117
+ const status = this.taskStatus(index, indexedTask, taskTurn?.status);
118
+ const liveObservable = indexedTask.status === 'open'
119
+ && index.tasks.at(-1)?.turnId === indexedTask.turnId;
120
+ return {
121
+ revision: trajectoryRevision(index, status),
122
+ status,
123
+ liveObservable,
124
+ session: {
125
+ id: `codex:${threadId}:${turnId}`,
126
+ threadId,
127
+ sourceThreadId: threadId,
128
+ sessionId: traceSession.runId,
129
+ sourceTrace: row.rolloutPath,
130
+ sourceKind: 'codex',
131
+ entrypoint: traceSession.entrypoint,
132
+ sourceMetadata: traceSession.sourceMetadata,
133
+ cwd: traceSession.cwd ?? row.cwd,
134
+ startTimestamp,
135
+ endTimestamp,
136
+ attributedEventIds: [],
137
+ turns,
138
+ fullSessionTimeline,
139
+ indicators: { userCorrectionCount: 0 },
140
+ },
141
+ ingestion: {
142
+ fileCount: 1,
143
+ sourceRecordCount: selected.lines.length,
144
+ parsedRecordCount: selected.lines.length - selected.malformedRecordCount,
145
+ malformedRecordCount: selected.malformedRecordCount,
146
+ ignoredValueCount: 0,
147
+ unknownEventCount: traceSession.events.filter((event) => event.eventKind === 'unknown').length,
148
+ filteredSessionCount: 0,
149
+ },
150
+ sourceRecords,
151
+ };
152
+ }
153
+ async loadLiveSnapshot(row, turnId, previous) {
154
+ const sourceStat = statSync(row.rolloutPath);
155
+ if (previous
156
+ && previous.revision === trajectoryRevisionFromStat(sourceStat, previous.value.status)) {
157
+ const status = previous.value.status === 'open' && !this.isLiveSource(sourceStat.mtimeMs)
158
+ ? 'unknown'
159
+ : previous.value.status;
160
+ if (status === previous.value.status)
161
+ return previous;
162
+ const value = {
163
+ ...previous.value,
164
+ revision: trajectoryRevisionFromStat(sourceStat, status),
165
+ status,
166
+ };
167
+ return {
168
+ revision: value.revision,
169
+ terminal: !value.liveObservable || isTerminalTaskStatus(status),
170
+ value,
171
+ };
172
+ }
173
+ const index = await this.currentIndexFor(row);
174
+ const indexedTask = index.tasks.find((task) => task.turnId === turnId);
175
+ if (!indexedTask)
176
+ throw new Error(`Codex 任务不存在:${turnId}`);
177
+ const value = this.trajectoryFromIndex(row, index, indexedTask);
178
+ return {
179
+ revision: value.revision,
180
+ terminal: !value.liveObservable || isTerminalTaskStatus(value.status),
181
+ value,
182
+ };
183
+ }
184
+ conversationSummary(row, cached) {
185
+ const liveTurnId = cached ? this.liveTurnId(cached) : undefined;
186
+ const tasks = cached ? taskItems(row.id, cached.tasks, liveTurnId) : [];
187
+ return {
188
+ threadId: row.id,
189
+ sourceThreadId: row.id,
190
+ sourceKind: 'codex',
191
+ title: conversationTitle(row),
192
+ preview: row.preview,
193
+ cwd: row.cwd,
194
+ model: row.model,
195
+ reasoningEffort: row.reasoningEffort,
196
+ archived: row.archived,
197
+ tokensUsed: row.tokensUsed,
198
+ childThreadCount: row.childThreadCount,
199
+ startTimestamp: timestampFromMs(row.createdAtMs),
200
+ endTimestamp: timestampFromMs(row.updatedAtMs),
201
+ durationMs: durationMsBetween(timestampFromMs(row.createdAtMs), timestampFromMs(row.updatedAtMs)),
202
+ ...(cached ? {
203
+ turnCount: tasks.length,
204
+ toolCallCount: tasks.reduce((sum, task) => sum + task.toolCallCount, 0),
205
+ toolFailureCount: tasks.reduce((sum, task) => sum + task.toolFailureCount, 0),
206
+ } : {}),
207
+ relatedSkillNames: [],
208
+ tasks,
209
+ };
210
+ }
211
+ taskStatus(index, task, reconstructed) {
212
+ if (task.status === 'unknown')
213
+ return 'unknown';
214
+ const observed = reconstructed ?? task.status;
215
+ if (observed !== 'open')
216
+ return observed;
217
+ return this.liveTurnId(index) === task.turnId ? 'open' : 'unknown';
218
+ }
219
+ liveTurnId(index) {
220
+ const lastTask = index.tasks.at(-1);
221
+ if (!lastTask || lastTask.status !== 'open')
222
+ return undefined;
223
+ return this.isLiveSource(index.sourceMtimeMs) ? lastTask.turnId : undefined;
224
+ }
225
+ isLiveSource(sourceMtimeMs) {
226
+ const ageMs = Math.max(0, this.now() - sourceMtimeMs);
227
+ return ageMs <= this.liveActivityWindowMs;
228
+ }
229
+ async conversationForOverview(row) {
230
+ const cached = this.readCachedIndex(row);
231
+ if (!cached) {
232
+ if (!this.isRecentRollout(row))
233
+ return this.conversationSummary(row, undefined);
234
+ const sourceSize = statSync(row.rolloutPath).size;
235
+ if (this.useBackgroundProcess && sourceSize >= this.backgroundProcessThresholdBytes) {
236
+ this.refreshIndexInBackground(row);
237
+ return this.conversationSummary(row, undefined);
238
+ }
239
+ try {
240
+ return this.conversationFromIndex(row, await this.currentIndexFor(row));
241
+ }
242
+ catch {
243
+ return this.conversationSummary(row, undefined);
244
+ }
245
+ }
246
+ if (isCurrentCodexRolloutIndex(cached, row.rolloutPath)) {
247
+ return this.conversationSummary(row, cached);
248
+ }
249
+ try {
250
+ return this.conversationFromIndex(row, await this.currentIndexFor(row));
251
+ }
252
+ catch {
253
+ return this.conversationFromIndex(row, cached);
254
+ }
255
+ }
256
+ isRecentRollout(row) {
257
+ if (!row.rolloutPath || !existsSync(row.rolloutPath))
258
+ return false;
259
+ try {
260
+ return this.isLiveSource(statSync(row.rolloutPath).mtimeMs);
261
+ }
262
+ catch {
263
+ return false;
264
+ }
265
+ }
266
+ conversationFromIndex(row, index) {
267
+ return this.conversationSummary(row, index);
268
+ }
269
+ async indexFor(row) {
270
+ const cached = this.readCachedIndex(row);
271
+ if (cached) {
272
+ if (!isCurrentCodexRolloutIndex(cached, row.rolloutPath))
273
+ this.refreshIndexInBackground(row);
274
+ return cached;
275
+ }
276
+ return this.awaitIndex(row);
277
+ }
278
+ async currentIndexFor(row) {
279
+ const cached = this.readCachedIndex(row);
280
+ if (cached && isCurrentCodexRolloutIndex(cached, row.rolloutPath))
281
+ return cached;
282
+ return this.awaitIndex(row);
283
+ }
284
+ async awaitIndex(row) {
285
+ const inFlight = this.indexPromises.get(row.id);
286
+ if (inFlight)
287
+ return inFlight;
288
+ const pending = this.buildIndex(row);
289
+ this.indexPromises.set(row.id, pending);
290
+ try {
291
+ return await pending;
292
+ }
293
+ finally {
294
+ this.indexPromises.delete(row.id);
295
+ }
296
+ }
297
+ refreshIndexInBackground(row) {
298
+ if (this.indexPromises.has(row.id))
299
+ return;
300
+ const pending = this.buildIndex(row);
301
+ this.indexPromises.set(row.id, pending);
302
+ void pending.catch(() => undefined).finally(() => {
303
+ if (this.indexPromises.get(row.id) === pending)
304
+ this.indexPromises.delete(row.id);
305
+ });
306
+ }
307
+ buildIndex(row) {
308
+ const sourceSize = statSync(row.rolloutPath).size;
309
+ const cachePath = this.cachePath(row.id);
310
+ const cached = this.readCachedIndex(row);
311
+ const pendingBytes = cached ? sourceSize - cached.indexedSize : sourceSize;
312
+ if (!this.useBackgroundProcess || pendingBytes < this.backgroundProcessThresholdBytes) {
313
+ const index = synchronizeCurrentCodexRolloutIndex(row.rolloutPath, row.id, cached);
314
+ writeJsonFileAtomic(cachePath, index);
315
+ return Promise.resolve(index);
316
+ }
317
+ return new Promise((resolve, reject) => {
318
+ let settled = false;
319
+ let stderr = '';
320
+ const processPath = fileURLToPath(new URL('./conversation-index-process.js', import.meta.url));
321
+ const child = spawn(process.execPath, [processPath, row.rolloutPath, row.id, cachePath], {
322
+ stdio: ['ignore', 'ignore', 'pipe'],
323
+ });
324
+ child.stderr.setEncoding('utf8');
325
+ child.stderr.on('data', (chunk) => {
326
+ if (stderr.length < 16_384)
327
+ stderr += chunk;
328
+ });
329
+ child.once('error', (cause) => {
330
+ if (settled)
331
+ return;
332
+ settled = true;
333
+ reject(cause);
334
+ });
335
+ child.once('exit', (code) => {
336
+ if (settled)
337
+ return;
338
+ settled = true;
339
+ if (code !== 0) {
340
+ reject(new Error(stderr.trim() || `Codex 对话索引进程异常退出:${code ?? 'unknown'}`));
341
+ return;
342
+ }
343
+ const cachedIndex = this.readCachedIndex(row);
344
+ if (!cachedIndex) {
345
+ reject(new Error('Codex 对话索引进程未生成有效缓存'));
346
+ return;
347
+ }
348
+ try {
349
+ const index = synchronizeCurrentCodexRolloutIndex(row.rolloutPath, row.id, cachedIndex);
350
+ writeJsonFileAtomic(cachePath, index);
351
+ resolve(index);
352
+ }
353
+ catch (cause) {
354
+ reject(cause);
355
+ }
356
+ });
357
+ });
358
+ }
359
+ readCachedIndex(row) {
360
+ const path = this.cachePath(row.id);
361
+ if (!existsSync(path) || !existsSync(row.rolloutPath))
362
+ return undefined;
363
+ try {
364
+ const value = JSON.parse(readFileSync(path, 'utf8'));
365
+ return isReusableCodexRolloutIndex(value, row.rolloutPath)
366
+ ? normalizeCodexRolloutIndex(value)
367
+ : undefined;
368
+ }
369
+ catch {
370
+ return undefined;
371
+ }
372
+ }
373
+ cachePath(threadId) {
374
+ const digest = createHash('sha256').update(threadId).digest('hex').slice(0, 24);
375
+ return join(this.cacheDir, `${digest}.json`);
376
+ }
377
+ listThreadRows() {
378
+ return this.readRowsFromStateDatabase() ?? this.readRowsFromSessionIndex();
379
+ }
380
+ findThreadRow(threadId) {
381
+ return this.readRowsFromStateDatabase(threadId)?.[0]
382
+ ?? this.readRowsFromSessionIndex().find((row) => row.id === threadId);
383
+ }
384
+ readRowsFromStateDatabase(threadId) {
385
+ const path = join(this.codexHome, 'state_5.sqlite');
386
+ if (!existsSync(path))
387
+ return undefined;
388
+ let database;
389
+ try {
390
+ database = new DatabaseSync(path, { readOnly: true });
391
+ const childCounts = childThreadCounts(database);
392
+ const where = `where (thread_source = 'user' or (thread_source is null and source not like '{"subagent"%'))${threadId ? ' and id = ?' : ''}`;
393
+ const statement = database.prepare(`
394
+ select id, rollout_path, created_at, updated_at, created_at_ms, updated_at_ms,
395
+ source, cwd, title, preview, first_user_message, archived,
396
+ tokens_used, model, reasoning_effort
397
+ from threads
398
+ ${where}
399
+ order by recency_at desc, updated_at desc
400
+ `);
401
+ const values = threadId ? statement.all(threadId) : statement.all();
402
+ return values.map((value) => threadRow(value, childCounts));
403
+ }
404
+ catch {
405
+ return undefined;
406
+ }
407
+ finally {
408
+ database?.close();
409
+ }
410
+ }
411
+ readRowsFromSessionIndex() {
412
+ const path = join(this.codexHome, 'session_index.jsonl');
413
+ if (!existsSync(path))
414
+ return [];
415
+ const rolloutPaths = new Map();
416
+ return readFileSync(path, 'utf8').split(/\r?\n/u).flatMap((line) => {
417
+ if (!line.trim())
418
+ return [];
419
+ try {
420
+ const value = JSON.parse(line);
421
+ const id = textValue(value.id);
422
+ if (!id)
423
+ return [];
424
+ let rolloutPath = rolloutPaths.get(id);
425
+ if (!rolloutPath) {
426
+ rolloutPath = findRolloutPath(join(this.codexHome, 'sessions'), id) ?? '';
427
+ rolloutPaths.set(id, rolloutPath);
428
+ }
429
+ return [{
430
+ id,
431
+ rolloutPath,
432
+ title: textValue(value.thread_name),
433
+ preview: textValue(value.thread_name),
434
+ updatedAtMs: numberValue(value.updated_at),
435
+ archived: false,
436
+ childThreadCount: 0,
437
+ }];
438
+ }
439
+ catch {
440
+ return [];
441
+ }
442
+ }).sort((left, right) => (right.updatedAtMs ?? 0) - (left.updatedAtMs ?? 0));
443
+ }
444
+ }
445
+ function threadRow(value, childCounts) {
446
+ const id = textValue(value.id) ?? '';
447
+ return {
448
+ id,
449
+ rolloutPath: textValue(value.rollout_path) ?? '',
450
+ createdAtMs: numberValue(value.created_at_ms) ?? secondsToMs(value.created_at),
451
+ updatedAtMs: numberValue(value.updated_at_ms) ?? secondsToMs(value.updated_at),
452
+ source: textValue(value.source),
453
+ cwd: textValue(value.cwd),
454
+ title: textValue(value.title),
455
+ preview: textValue(value.preview),
456
+ firstUserMessage: textValue(value.first_user_message),
457
+ archived: numberValue(value.archived) === 1,
458
+ tokensUsed: numberValue(value.tokens_used),
459
+ model: textValue(value.model),
460
+ reasoningEffort: textValue(value.reasoning_effort),
461
+ childThreadCount: childCounts.get(id) ?? 0,
462
+ };
463
+ }
464
+ function childThreadCounts(database) {
465
+ const counts = new Map();
466
+ try {
467
+ const rows = database.prepare(`
468
+ select parent_thread_id, count(*) as child_count
469
+ from thread_spawn_edges
470
+ group by parent_thread_id
471
+ `).all();
472
+ for (const row of rows) {
473
+ const parent = textValue(row.parent_thread_id);
474
+ if (parent)
475
+ counts.set(parent, numberValue(row.child_count) ?? 0);
476
+ }
477
+ }
478
+ catch {
479
+ // Older Codex state databases may not expose spawn edges.
480
+ }
481
+ return counts;
482
+ }
483
+ function taskItems(threadId, tasks, liveTurnId) {
484
+ return tasks.map((task) => ({
485
+ turnId: task.turnId,
486
+ sourceTurnId: task.turnId,
487
+ trajectoryHref: `/conversations/${encodeURIComponent(threadId)}/tasks/${encodeURIComponent(task.turnId)}`,
488
+ title: task.title,
489
+ startTimestamp: task.startTimestamp,
490
+ endTimestamp: task.endTimestamp,
491
+ durationMs: durationMsBetween(task.startTimestamp, task.endTimestamp),
492
+ status: task.status === 'open' && task.turnId !== liveTurnId ? 'unknown' : task.status,
493
+ eventCount: task.sourceRecordCount,
494
+ toolCallCount: task.toolCallCount,
495
+ toolFailureCount: task.toolFailureCount,
496
+ relatedSkillNames: [],
497
+ }));
498
+ }
499
+ function sourceRecordView(lines, traceId, sourceTrace) {
500
+ let byteCount = 0;
501
+ let omittedRecordCount = 0;
502
+ const records = lines.flatMap((line) => {
503
+ const record = observationSourceRecordFromLine(line.text, line.line, traceId, sourceTrace);
504
+ if (byteCount + record.byteCount > MAX_SOURCE_RECORD_ARCHIVE_BYTES) {
505
+ omittedRecordCount += 1;
506
+ return [];
507
+ }
508
+ byteCount += record.byteCount;
509
+ return [record];
510
+ });
511
+ const truncated = omittedRecordCount > 0 || records.some((record) => record.truncated);
512
+ return {
513
+ status: truncated ? 'partial' : 'available',
514
+ recordCount: records.length,
515
+ records,
516
+ omittedRecordCount,
517
+ byteCount,
518
+ truncated,
519
+ ...(truncated ? { reason: 'archive_limit' } : {}),
520
+ };
521
+ }
522
+ function conversationTitle(row) {
523
+ return row.title?.trim()
524
+ || row.preview?.trim()
525
+ || row.firstUserMessage?.trim()
526
+ || '未命名对话';
527
+ }
528
+ function timestampFromMs(value) {
529
+ if (value === undefined || !Number.isFinite(value))
530
+ return undefined;
531
+ return new Date(value).toISOString();
532
+ }
533
+ function rolloutRevision(index) {
534
+ return `${index.sourceSize}:${index.sourceMtimeMs}`;
535
+ }
536
+ function trajectoryRevision(index, status) {
537
+ return `${rolloutRevision(index)}:${status}`;
538
+ }
539
+ function trajectoryRevisionFromStat(sourceStat, status) {
540
+ return `${sourceStat.size}:${sourceStat.mtimeMs}:${status}`;
541
+ }
542
+ function isTerminalTaskStatus(status) {
543
+ return status === 'completed' || status === 'aborted' || status === 'interrupted';
544
+ }
545
+ function secondsToMs(value) {
546
+ const seconds = numberValue(value);
547
+ return seconds === undefined ? undefined : seconds * 1000;
548
+ }
549
+ function textValue(value) {
550
+ return typeof value === 'string' && value.trim() ? value : undefined;
551
+ }
552
+ function numberValue(value) {
553
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
554
+ }
555
+ function findRolloutPath(root, threadId) {
556
+ if (!existsSync(root))
557
+ return undefined;
558
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
559
+ const path = join(root, entry.name);
560
+ if (entry.isDirectory()) {
561
+ const nested = findRolloutPath(path, threadId);
562
+ if (nested)
563
+ return nested;
564
+ }
565
+ else if (entry.isFile() && entry.name.includes(threadId) && entry.name.endsWith('.jsonl')) {
566
+ return path;
567
+ }
568
+ }
569
+ return undefined;
570
+ }
571
+ export function conversationSourceLabel(path) {
572
+ return basename(path).replace(/\.jsonl$/u, '');
573
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { buildCodexRolloutIndex, extendCodexRolloutIndex, isReusableCodexRolloutIndex, } from './codex-conversation-index.js';
3
+ import { writeJsonFileAtomic } from '../shared/atomic-json.js';
4
+ const [sourcePath, sourceThreadId, cachePath] = process.argv.slice(2);
5
+ if (!sourcePath || !sourceThreadId || !cachePath) {
6
+ console.error('Codex 对话索引进程缺少必要参数');
7
+ process.exitCode = 1;
8
+ }
9
+ else {
10
+ try {
11
+ const cached = readCachedIndex(cachePath);
12
+ const index = cached && isReusableCodexRolloutIndex(cached, sourcePath)
13
+ ? extendCodexRolloutIndex(sourcePath, sourceThreadId, cached)
14
+ : buildCodexRolloutIndex(sourcePath, sourceThreadId);
15
+ writeJsonFileAtomic(cachePath, index);
16
+ }
17
+ catch (cause) {
18
+ console.error(cause instanceof Error ? cause.message : String(cause));
19
+ process.exitCode = 1;
20
+ }
21
+ }
22
+ function readCachedIndex(cachePath) {
23
+ if (!existsSync(cachePath))
24
+ return undefined;
25
+ try {
26
+ return JSON.parse(readFileSync(cachePath, 'utf8'));
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
@@ -0,0 +1,5 @@
1
+ import type { ConversationIndexViewModel } from '../types/index.js';
2
+ import type { ConversationCatalog } from './conversation-catalog.js';
3
+ export declare function buildConversationIndexViewModel(observationsDir: string): ConversationIndexViewModel;
4
+ /** Test/compatibility adapter for persisted observe reports. */
5
+ export declare function createObservationConversationCatalog(observationsDir: string): ConversationCatalog;