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,566 @@
1
+ import { closeSync, openSync, readSync, statSync } from 'node:fs';
2
+ import { codexUserDisplayText, codexUserMessageOrigin } from './codex-protocol.js';
3
+ import { codexRuntimeToolOutcomeFromPayload, codexToolOutputOutcome, } from './codex-tool-status.js';
4
+ const READ_CHUNK_BYTES = 256 * 1024;
5
+ const MAX_RECORD_BYTES = 32 * 1024 * 1024;
6
+ const INDEX_SCHEMA_VERSION = 11;
7
+ const MAX_CURRENT_INDEX_ATTEMPTS = 4;
8
+ /**
9
+ * Build a compact byte-range index without retaining the rollout. Only records
10
+ * needed for turn boundaries and list metrics are parsed.
11
+ */
12
+ export function buildCodexRolloutIndex(sourcePath, sourceThreadId) {
13
+ const sourceStat = statSync(sourcePath);
14
+ const state = scanCodexRolloutIndex(sourcePath, sourceThreadId, {
15
+ tasks: [],
16
+ sourceRecordCount: 0,
17
+ malformedRecordCount: 0,
18
+ indexedSize: 0,
19
+ indexedLineCount: 0,
20
+ indexedEndsWithNewline: true,
21
+ }, 0, sourceStat.size, 0);
22
+ return indexFromState(sourcePath, sourceThreadId, sourceStat, state);
23
+ }
24
+ /**
25
+ * Extend an append-only rollout index from its last byte boundary. If the
26
+ * source was replaced or truncated, rebuild so callers never observe a mixed
27
+ * index from two different files.
28
+ */
29
+ export function extendCodexRolloutIndex(sourcePath, sourceThreadId, previous) {
30
+ const sourceStat = statSync(sourcePath);
31
+ if (!isReusableCodexRolloutIndex(previous, sourcePath)
32
+ || previous.sourceThreadId !== sourceThreadId
33
+ || sourceStat.size < previous.sourceSize) {
34
+ return buildCodexRolloutIndex(sourcePath, sourceThreadId);
35
+ }
36
+ const normalizedPrevious = normalizeCodexRolloutIndex(previous);
37
+ if (sourceStat.size === previous.sourceSize && sourceStat.mtimeMs === previous.sourceMtimeMs) {
38
+ return normalizedPrevious;
39
+ }
40
+ const tasks = normalizedPrevious.tasks.map((task) => ({ ...task }));
41
+ const resume = resumeAppendCursor(sourcePath, normalizedPrevious, sourceStat.size);
42
+ if (!resume)
43
+ return buildCodexRolloutIndex(sourcePath, sourceThreadId);
44
+ completeTailTaskDelimiter(tasks, normalizedPrevious.indexedSize, resume.indexedSize);
45
+ const lastTask = tasks.at(-1);
46
+ const active = lastTask?.status === 'open'
47
+ ? mutableTask(tasks.pop(), normalizedPrevious.activeToolOutcomeState)
48
+ : undefined;
49
+ const state = {
50
+ tasks,
51
+ active,
52
+ sessionMeta: normalizedPrevious.sessionMeta,
53
+ sessionMetaLine: normalizedPrevious.sessionMetaLine,
54
+ sourceRecordCount: normalizedPrevious.sourceRecordCount,
55
+ malformedRecordCount: normalizedPrevious.malformedRecordCount,
56
+ indexedSize: resume.indexedSize,
57
+ indexedLineCount: normalizedPrevious.indexedLineCount,
58
+ indexedEndsWithNewline: resume.indexedEndsWithNewline,
59
+ };
60
+ const extended = resume.indexedSize < sourceStat.size
61
+ ? scanCodexRolloutIndex(sourcePath, sourceThreadId, state, resume.indexedSize, sourceStat.size, normalizedPrevious.indexedLineCount)
62
+ : state;
63
+ return indexFromState(sourcePath, sourceThreadId, sourceStat, extended);
64
+ }
65
+ /**
66
+ * Catch an append-only rollout up to a snapshot that is still current after
67
+ * indexing. Returning a stale prefix here would violate current-read callers.
68
+ */
69
+ export function synchronizeCurrentCodexRolloutIndex(sourcePath, sourceThreadId, initial) {
70
+ let index = initial
71
+ && initial.sourceThreadId === sourceThreadId
72
+ && isReusableCodexRolloutIndex(initial, sourcePath)
73
+ ? normalizeCodexRolloutIndex(initial)
74
+ : buildCodexRolloutIndex(sourcePath, sourceThreadId);
75
+ for (let attempt = 0; attempt < MAX_CURRENT_INDEX_ATTEMPTS; attempt += 1) {
76
+ if (isCurrentCodexRolloutIndex(index, sourcePath))
77
+ return index;
78
+ index = extendCodexRolloutIndex(sourcePath, sourceThreadId, index);
79
+ }
80
+ if (isCurrentCodexRolloutIndex(index, sourcePath))
81
+ return index;
82
+ throw new Error('Codex 对话日志持续写入,暂时无法形成当前索引快照');
83
+ }
84
+ function resumeAppendCursor(sourcePath, previous, sourceSize) {
85
+ if (previous.indexedEndsWithNewline) {
86
+ return {
87
+ indexedSize: previous.indexedSize,
88
+ indexedEndsWithNewline: true,
89
+ };
90
+ }
91
+ const fd = openSync(sourcePath, 'r');
92
+ const buffer = Buffer.allocUnsafe(Math.min(4_096, Math.max(1, sourceSize - previous.indexedSize)));
93
+ let offset = previous.indexedSize;
94
+ try {
95
+ while (offset < sourceSize) {
96
+ const bytesRead = readSync(fd, buffer, 0, Math.min(buffer.length, sourceSize - offset), offset);
97
+ if (bytesRead === 0)
98
+ break;
99
+ for (let index = 0; index < bytesRead; index += 1) {
100
+ const byte = buffer[index];
101
+ if (byte === 0x0a) {
102
+ return {
103
+ indexedSize: offset + index + 1,
104
+ indexedEndsWithNewline: true,
105
+ };
106
+ }
107
+ if (byte !== 0x09 && byte !== 0x0d && byte !== 0x20)
108
+ return undefined;
109
+ }
110
+ offset += bytesRead;
111
+ }
112
+ }
113
+ finally {
114
+ closeSync(fd);
115
+ }
116
+ return {
117
+ indexedSize: sourceSize,
118
+ indexedEndsWithNewline: false,
119
+ };
120
+ }
121
+ function completeTailTaskDelimiter(tasks, previousIndexedSize, indexedSize) {
122
+ if (indexedSize <= previousIndexedSize)
123
+ return;
124
+ const tailTask = tasks.at(-1);
125
+ if (tailTask?.endOffset === previousIndexedSize)
126
+ tailTask.endOffset = indexedSize;
127
+ }
128
+ /**
129
+ * A single Codex rollout is sequential: once a newer task starts, an older
130
+ * task without a terminal record can no longer be live. Normalize historical
131
+ * caches in memory so an omitted task_complete never becomes a permanent
132
+ * "running" conversation.
133
+ */
134
+ export function normalizeCodexRolloutIndex(index) {
135
+ let changed = false;
136
+ const tasks = index.tasks.map((task, taskIndex) => {
137
+ if (task.status !== 'open' || taskIndex === index.tasks.length - 1)
138
+ return task;
139
+ changed = true;
140
+ return {
141
+ ...task,
142
+ status: 'unknown',
143
+ endTimestamp: task.endTimestamp ?? index.tasks[taskIndex + 1]?.startTimestamp,
144
+ };
145
+ });
146
+ return changed ? { ...index, tasks } : index;
147
+ }
148
+ function scanCodexRolloutIndex(sourcePath, sourceThreadId, state, startOffset, endOffset, startLine) {
149
+ const scanned = forEachJsonlLine(sourcePath, (record) => {
150
+ state.sourceRecordCount += 1;
151
+ const relevant = isIndexRelevantLine(record.text);
152
+ if (!relevant) {
153
+ includeRecord(state.active, record);
154
+ return;
155
+ }
156
+ let parsed;
157
+ try {
158
+ parsed = JSON.parse(record.text);
159
+ }
160
+ catch {
161
+ state.malformedRecordCount += 1;
162
+ includeRecord(state.active, record);
163
+ return;
164
+ }
165
+ const raw = objectValue(parsed);
166
+ if (!raw)
167
+ return;
168
+ const recordType = stringValue(raw.type);
169
+ const payload = objectValue(raw.payload) ?? {};
170
+ const payloadType = stringValue(payload.type);
171
+ const timestamp = stringValue(raw.timestamp);
172
+ if (recordType === 'session_meta' && state.sessionMeta === undefined) {
173
+ state.sessionMeta = parsed;
174
+ state.sessionMetaLine = record.line;
175
+ return;
176
+ }
177
+ if (recordType === 'event_msg' && payloadType === 'task_started') {
178
+ if (state.active) {
179
+ state.tasks.push(finalizeSupersededTask(state.active, record.startOffset, record.line - 1, timestamp));
180
+ }
181
+ const nativeTurnId = stringValue(payload.turn_id);
182
+ state.active = newMutableTask(nativeTurnId ?? `turn:${sourceThreadId}:${record.line}`, record, timestamp);
183
+ return;
184
+ }
185
+ includeRecord(state.active, record);
186
+ if (isUserPromptRecord(recordType, payloadType, payload)) {
187
+ const message = userPromptText(payload)?.trim();
188
+ const displayText = message && codexUserMessageOrigin(message) === 'human'
189
+ ? codexUserDisplayText(message)?.trim()
190
+ : undefined;
191
+ if (!displayText)
192
+ return;
193
+ state.active ??= newMutableTask(`turn:${sourceThreadId}:${record.line}`, record, timestamp);
194
+ const titlePriority = userPromptPriority(recordType, payloadType);
195
+ if (titlePriority > state.active.titlePriority) {
196
+ state.active.title = compactTitle(displayText);
197
+ state.active.titlePriority = titlePriority;
198
+ }
199
+ return;
200
+ }
201
+ if (!state.active)
202
+ return;
203
+ if (recordType === 'response_item' && isToolCallPayload(payloadType)) {
204
+ recordToolCall(state.active, record, payload);
205
+ }
206
+ recordToolOutcome(state.active, record, recordType, payloadType, payload);
207
+ if (recordType === 'event_msg' && payloadType === 'task_complete') {
208
+ state.active.status = 'completed';
209
+ state.active.endTimestamp = timestamp;
210
+ if (state.active.titlePriority === 0) {
211
+ const fallback = stringValue(payload.last_agent_message)?.trim();
212
+ if (fallback)
213
+ state.active.title = compactTitle(fallback);
214
+ }
215
+ state.tasks.push(stripMutable(state.active));
216
+ state.active = undefined;
217
+ return;
218
+ }
219
+ if (recordType === 'event_msg' && (payloadType === 'turn_aborted' || payloadType === 'turn_interrupted')) {
220
+ state.active.status = payloadType === 'turn_aborted' ? 'aborted' : 'interrupted';
221
+ state.active.endTimestamp = timestamp;
222
+ state.tasks.push(stripMutable(state.active));
223
+ state.active = undefined;
224
+ }
225
+ }, startOffset, endOffset, startLine);
226
+ state.indexedSize = scanned.indexedSize;
227
+ state.indexedLineCount = scanned.indexedLineCount;
228
+ state.indexedEndsWithNewline = scanned.indexedEndsWithNewline;
229
+ return state;
230
+ }
231
+ function indexFromState(sourcePath, sourceThreadId, sourceStat, state) {
232
+ const tasks = state.active
233
+ ? [...state.tasks, stripMutable(state.active)]
234
+ : state.tasks;
235
+ return {
236
+ schemaVersion: INDEX_SCHEMA_VERSION,
237
+ sourcePath,
238
+ sourceSize: sourceStat.size,
239
+ sourceMtimeMs: sourceStat.mtimeMs,
240
+ indexedSize: state.indexedSize,
241
+ indexedLineCount: state.indexedLineCount,
242
+ indexedEndsWithNewline: state.indexedEndsWithNewline,
243
+ sourceThreadId,
244
+ sessionMeta: state.sessionMeta,
245
+ sessionMetaLine: state.sessionMetaLine,
246
+ tasks,
247
+ activeToolOutcomeState: state.active
248
+ ? serializeToolOutcomeTracker(state.active.toolOutcomeTracker)
249
+ : undefined,
250
+ sourceRecordCount: state.sourceRecordCount,
251
+ malformedRecordCount: state.malformedRecordCount,
252
+ };
253
+ }
254
+ function newMutableTask(turnId, record, startTimestamp) {
255
+ return {
256
+ turnId,
257
+ title: '未命名任务',
258
+ titlePriority: 0,
259
+ startTimestamp,
260
+ status: 'open',
261
+ sourceRecordCount: 1,
262
+ toolCallCount: 0,
263
+ toolFailureCount: 0,
264
+ startOffset: record.startOffset,
265
+ endOffset: record.endOffset,
266
+ startLine: record.line,
267
+ endLine: record.line,
268
+ toolOutcomeTracker: createToolOutcomeTracker(),
269
+ };
270
+ }
271
+ function mutableTask(task, activeToolOutcomeState) {
272
+ return {
273
+ ...task,
274
+ titlePriority: task.title === '未命名任务' ? 0 : 1,
275
+ toolOutcomeTracker: createToolOutcomeTracker(activeToolOutcomeState),
276
+ };
277
+ }
278
+ export function isCurrentCodexRolloutIndex(value, sourcePath) {
279
+ const candidate = objectValue(value);
280
+ if (!candidate || candidate.schemaVersion !== INDEX_SCHEMA_VERSION || candidate.sourcePath !== sourcePath)
281
+ return false;
282
+ const sourceSize = numberValue(candidate.sourceSize);
283
+ const indexedSize = numberValue(candidate.indexedSize);
284
+ const indexedLineCount = numberValue(candidate.indexedLineCount);
285
+ if (sourceSize === undefined || indexedSize === undefined || indexedLineCount === undefined
286
+ || typeof candidate.indexedEndsWithNewline !== 'boolean')
287
+ return false;
288
+ if (indexedSize > sourceSize || !Array.isArray(candidate.tasks))
289
+ return false;
290
+ try {
291
+ const current = statSync(sourcePath);
292
+ return sourceSize === current.size
293
+ && candidate.sourceMtimeMs === current.mtimeMs
294
+ && indexedSize <= current.size;
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ /**
301
+ * Codex rollouts are append-only while a conversation is active. A prefix
302
+ * index remains internally consistent even after newer records are appended.
303
+ */
304
+ export function isReusableCodexRolloutIndex(value, sourcePath) {
305
+ const candidate = objectValue(value);
306
+ if (!candidate || candidate.schemaVersion !== INDEX_SCHEMA_VERSION || candidate.sourcePath !== sourcePath)
307
+ return false;
308
+ if (!Array.isArray(candidate.tasks))
309
+ return false;
310
+ try {
311
+ const current = statSync(sourcePath);
312
+ const sourceSize = numberValue(candidate.sourceSize);
313
+ const sourceMtimeMs = numberValue(candidate.sourceMtimeMs);
314
+ const indexedSize = numberValue(candidate.indexedSize);
315
+ const indexedLineCount = numberValue(candidate.indexedLineCount);
316
+ if (sourceSize === undefined || sourceMtimeMs === undefined
317
+ || indexedSize === undefined || indexedLineCount === undefined
318
+ || typeof candidate.indexedEndsWithNewline !== 'boolean')
319
+ return false;
320
+ if (indexedSize > sourceSize || sourceSize > current.size)
321
+ return false;
322
+ if (current.size === sourceSize)
323
+ return current.mtimeMs === sourceMtimeMs;
324
+ return current.size > sourceSize && current.mtimeMs >= sourceMtimeMs;
325
+ }
326
+ catch {
327
+ return false;
328
+ }
329
+ }
330
+ export function readCodexTaskRecords(index, task) {
331
+ const records = [];
332
+ if (index.sessionMeta !== undefined && index.sessionMetaLine !== undefined) {
333
+ records[index.sessionMetaLine] = index.sessionMeta;
334
+ }
335
+ const lines = [];
336
+ let malformedRecordCount = 0;
337
+ forEachJsonlLine(index.sourcePath, (record) => {
338
+ if (record.endOffset <= task.startOffset || record.startOffset >= task.endOffset)
339
+ return;
340
+ lines.push(record);
341
+ try {
342
+ records[record.line] = JSON.parse(record.text);
343
+ }
344
+ catch {
345
+ malformedRecordCount += 1;
346
+ }
347
+ }, task.startOffset, task.endOffset, task.startLine);
348
+ return { records, lines, malformedRecordCount };
349
+ }
350
+ function finalizeSupersededTask(task, endOffset, endLine, endTimestamp) {
351
+ return stripMutable({
352
+ ...task,
353
+ status: 'unknown',
354
+ endTimestamp: task.endTimestamp ?? endTimestamp,
355
+ endOffset: Math.max(task.endOffset, endOffset),
356
+ endLine: Math.max(task.endLine, endLine),
357
+ });
358
+ }
359
+ function stripMutable({ titlePriority: _titlePriority, toolOutcomeTracker: _toolOutcomeTracker, ...task }) {
360
+ return task;
361
+ }
362
+ function includeRecord(task, record) {
363
+ if (!task)
364
+ return;
365
+ task.sourceRecordCount += 1;
366
+ task.endOffset = record.endOffset;
367
+ task.endLine = record.line;
368
+ }
369
+ function isIndexRelevantLine(line) {
370
+ return line.includes('"type":"session_meta"')
371
+ || line.includes('"type":"task_started"')
372
+ || line.includes('"type":"task_complete"')
373
+ || line.includes('"type":"turn_aborted"')
374
+ || line.includes('"type":"turn_interrupted"')
375
+ || line.includes('"type":"user_message"')
376
+ || line.includes('"type":"message"')
377
+ || line.includes('"type":"function_call"')
378
+ || line.includes('"type":"custom_tool_call"')
379
+ || line.includes('"type":"local_shell_call"')
380
+ || line.includes('"type":"function_call_output"')
381
+ || line.includes('"type":"custom_tool_call_output"')
382
+ || line.includes('"type":"mcp_tool_call_end"')
383
+ || line.includes('"type":"patch_apply_end"');
384
+ }
385
+ function isToolCallPayload(payloadType) {
386
+ return payloadType === 'function_call'
387
+ || payloadType === 'custom_tool_call'
388
+ || payloadType === 'local_shell_call';
389
+ }
390
+ function isUserPromptRecord(recordType, payloadType, payload) {
391
+ return (recordType === 'event_msg' && payloadType === 'user_message')
392
+ || (recordType === 'response_item' && payloadType === 'message' && payload.role === 'user');
393
+ }
394
+ function userPromptPriority(recordType, payloadType) {
395
+ return recordType === 'event_msg' && payloadType === 'user_message' ? 2 : 1;
396
+ }
397
+ function userPromptText(payload) {
398
+ const direct = stringValue(payload.message) ?? stringValue(payload.content);
399
+ if (direct)
400
+ return direct;
401
+ if (!Array.isArray(payload.content))
402
+ return undefined;
403
+ const parts = payload.content.flatMap((value) => {
404
+ const item = objectValue(value);
405
+ const text = item ? stringValue(item.text) : undefined;
406
+ return text ? [text] : [];
407
+ });
408
+ return parts.length > 0 ? parts.join('\n') : undefined;
409
+ }
410
+ function recordToolOutcome(task, record, recordType, payloadType, payload) {
411
+ if (recordType === 'response_item'
412
+ && (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output')) {
413
+ const callId = stringValue(payload.call_id) ?? stringValue(payload.id) ?? `result:${record.line}`;
414
+ const occurrence = takeToolOccurrence(task.toolOutcomeTracker.resultOccurrences, callId);
415
+ updateToolOutcome(task, toolOccurrenceKey(callId, occurrence), 'outputStatus', codexToolOutputOutcome(payload.output, payload.status).status);
416
+ return;
417
+ }
418
+ if (recordType !== 'event_msg'
419
+ || (payloadType !== 'mcp_tool_call_end' && payloadType !== 'patch_apply_end'))
420
+ return;
421
+ const callId = stringValue(payload.call_id) ?? stringValue(payload.id) ?? `runtime:${record.line}`;
422
+ const occurrence = takeToolOccurrence(task.toolOutcomeTracker.runtimeOccurrences, callId);
423
+ ensureRepresentedToolCall(task, callId, occurrence);
424
+ const outcome = codexRuntimeToolOutcomeFromPayload(payloadType, payload);
425
+ if (!outcome.present)
426
+ return;
427
+ updateToolOutcome(task, toolOccurrenceKey(callId, occurrence), 'runtimeStatus', outcome.status);
428
+ }
429
+ function recordToolCall(task, record, payload) {
430
+ const callId = stringValue(payload.call_id) ?? stringValue(payload.id) ?? `call:${record.line}`;
431
+ takeToolOccurrence(task.toolOutcomeTracker.callOccurrences, callId);
432
+ task.toolCallCount += 1;
433
+ }
434
+ /** Match Trace IR's contract: a standalone runtime end still represents one tool call. */
435
+ function ensureRepresentedToolCall(task, callId, occurrence) {
436
+ const representedCount = task.toolOutcomeTracker.callOccurrences.get(callId) ?? 0;
437
+ const requiredCount = occurrence + 1;
438
+ if (representedCount >= requiredCount)
439
+ return;
440
+ task.toolOutcomeTracker.callOccurrences.set(callId, requiredCount);
441
+ task.toolCallCount += requiredCount - representedCount;
442
+ }
443
+ function updateToolOutcome(task, key, source, status) {
444
+ const previous = task.toolOutcomeTracker.outcomes.get(key) ?? {};
445
+ const wasFailure = resolvedToolStatus(previous) === 'failure';
446
+ const next = { ...previous, [source]: status };
447
+ task.toolOutcomeTracker.outcomes.set(key, next);
448
+ const isFailure = resolvedToolStatus(next) === 'failure';
449
+ if (wasFailure !== isFailure)
450
+ task.toolFailureCount += isFailure ? 1 : -1;
451
+ }
452
+ function resolvedToolStatus(outcome) {
453
+ return outcome.runtimeStatus ?? outcome.outputStatus ?? 'unknown';
454
+ }
455
+ function takeToolOccurrence(counts, callId) {
456
+ const occurrence = counts.get(callId) ?? 0;
457
+ counts.set(callId, occurrence + 1);
458
+ return occurrence;
459
+ }
460
+ function toolOccurrenceKey(callId, occurrence) {
461
+ return `${callId}\u0000${occurrence}`;
462
+ }
463
+ function createToolOutcomeTracker(state) {
464
+ return {
465
+ callOccurrences: new Map(state?.callOccurrences ?? []),
466
+ resultOccurrences: new Map(state?.resultOccurrences ?? []),
467
+ runtimeOccurrences: new Map(state?.runtimeOccurrences ?? []),
468
+ outcomes: new Map(state?.outcomes ?? []),
469
+ };
470
+ }
471
+ function serializeToolOutcomeTracker(tracker) {
472
+ return {
473
+ callOccurrences: [...tracker.callOccurrences],
474
+ resultOccurrences: [...tracker.resultOccurrences],
475
+ runtimeOccurrences: [...tracker.runtimeOccurrences],
476
+ outcomes: [...tracker.outcomes],
477
+ };
478
+ }
479
+ function compactTitle(value) {
480
+ return (codexUserDisplayText(value) ?? '').replace(/\s+/gu, ' ').trim().slice(0, 180);
481
+ }
482
+ function objectValue(value) {
483
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
484
+ ? value
485
+ : undefined;
486
+ }
487
+ function stringValue(value) {
488
+ return typeof value === 'string' && value.trim() ? value : undefined;
489
+ }
490
+ function numberValue(value) {
491
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
492
+ }
493
+ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.POSITIVE_INFINITY, startLine = 0) {
494
+ const fd = openSync(filePath, 'r');
495
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
496
+ let absoluteOffset = startOffset;
497
+ let lineStartOffset = startOffset;
498
+ let lineNumber = startLine;
499
+ let indexedSize = startOffset;
500
+ let indexedLineCount = startLine;
501
+ let indexedEndsWithNewline = true;
502
+ let fragments = [];
503
+ try {
504
+ while (absoluteOffset < endOffset) {
505
+ const requestBytes = Math.min(buffer.length, endOffset - absoluteOffset);
506
+ const bytesRead = readSync(fd, buffer, 0, requestBytes, absoluteOffset);
507
+ if (bytesRead === 0)
508
+ break;
509
+ let cursor = 0;
510
+ for (let index = 0; index < bytesRead; index += 1) {
511
+ if (buffer[index] !== 0x0a)
512
+ continue;
513
+ const tail = buffer.subarray(cursor, index);
514
+ const lineBuffer = fragments.length > 0
515
+ ? Buffer.concat([...fragments, tail])
516
+ : tail;
517
+ const nextOffset = absoluteOffset + index + 1;
518
+ emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
519
+ indexedSize = nextOffset;
520
+ indexedLineCount = lineNumber + 1;
521
+ indexedEndsWithNewline = true;
522
+ fragments = [];
523
+ cursor = index + 1;
524
+ lineStartOffset = nextOffset;
525
+ lineNumber += 1;
526
+ }
527
+ if (cursor < bytesRead)
528
+ fragments.push(Buffer.from(buffer.subarray(cursor, bytesRead)));
529
+ if (fragments.reduce((sum, item) => sum + item.length, 0) > MAX_RECORD_BYTES) {
530
+ throw new Error(`Codex JSONL 单条记录超过 ${MAX_RECORD_BYTES} 字节上限:${filePath}`);
531
+ }
532
+ absoluteOffset += bytesRead;
533
+ }
534
+ if (fragments.length > 0) {
535
+ const trailing = Buffer.concat(fragments);
536
+ if (isCompleteJsonRecord(trailing)) {
537
+ emitLine(trailing, lineNumber, lineStartOffset, absoluteOffset, visit);
538
+ indexedSize = absoluteOffset;
539
+ indexedLineCount = lineNumber + 1;
540
+ indexedEndsWithNewline = false;
541
+ }
542
+ }
543
+ }
544
+ finally {
545
+ closeSync(fd);
546
+ }
547
+ return { indexedSize, indexedLineCount, indexedEndsWithNewline };
548
+ }
549
+ function isCompleteJsonRecord(buffer) {
550
+ const text = buffer.toString('utf8').trim();
551
+ if (!text)
552
+ return false;
553
+ try {
554
+ JSON.parse(text);
555
+ return true;
556
+ }
557
+ catch {
558
+ return false;
559
+ }
560
+ }
561
+ function emitLine(buffer, line, startOffset, endOffset, visit) {
562
+ const text = buffer.toString('utf8').trim();
563
+ if (!text)
564
+ return;
565
+ visit({ text, line, startOffset, endOffset });
566
+ }
@@ -0,0 +1,12 @@
1
+ /** Stable inventory of Codex rollout record shapes understood by OMK. */
2
+ export declare function isCodexResponseItemType(value: string | undefined): boolean;
3
+ export declare function isCodexEventMessageType(value: string | undefined): boolean;
4
+ /** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
5
+ export declare function codexUserDisplayText(text: string): string | undefined;
6
+ export declare function codexUserMessageOrigin(text: string): 'human' | 'runtime';
7
+ /**
8
+ * Records consumed by correlation or retained only as source provenance do not
9
+ * emit a standalone Trace IR event. Keeping this inventory explicit preserves
10
+ * `unknown` as a forward-compatibility warning.
11
+ */
12
+ export declare function isCodexRecordConsumedWithoutDirectEvent(recordType: unknown, payloadType: string | undefined): boolean;
@@ -0,0 +1,78 @@
1
+ /** Stable inventory of Codex rollout record shapes understood by OMK. */
2
+ const RESPONSE_ITEM_TYPES = new Set([
3
+ 'message',
4
+ 'reasoning',
5
+ 'tool_search_call',
6
+ 'tool_search_output',
7
+ 'web_search_call',
8
+ 'image_generation_call',
9
+ 'function_call',
10
+ 'custom_tool_call',
11
+ 'function_call_output',
12
+ 'custom_tool_call_output',
13
+ 'agent_message',
14
+ ]);
15
+ const EVENT_MESSAGE_TYPES = new Set([
16
+ 'token_count',
17
+ 'task_started',
18
+ 'task_complete',
19
+ 'turn_aborted',
20
+ 'turn_interrupted',
21
+ 'user_message',
22
+ 'agent_message',
23
+ 'mcp_tool_call_end',
24
+ 'patch_apply_end',
25
+ 'agent_reasoning',
26
+ 'thread_settings_applied',
27
+ 'web_search_end',
28
+ 'context_compacted',
29
+ 'image_generation_end',
30
+ 'thread_goal_updated',
31
+ 'sub_agent_activity',
32
+ ]);
33
+ const IN_APP_BROWSER_CONTEXT_RE = /<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>/gi;
34
+ const CODEX_RUNTIME_MESSAGE_RE = /^# AGENTS\.md instructions\b|^<(?:app-context|environment_context|permissions instructions|collaboration_mode|apps_instructions|plugins_instructions|skills_instructions|recommended_plugins)>/i;
35
+ export function isCodexResponseItemType(value) {
36
+ return value !== undefined && RESPONSE_ITEM_TYPES.has(value);
37
+ }
38
+ export function isCodexEventMessageType(value) {
39
+ return value !== undefined && EVENT_MESSAGE_TYPES.has(value);
40
+ }
41
+ /** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
42
+ export function codexUserDisplayText(text) {
43
+ const requestHeading = /^## My request for Codex:\s*$/im;
44
+ const requestMatch = requestHeading.exec(text);
45
+ const request = requestMatch
46
+ ? text.slice(requestMatch.index + requestMatch[0].length)
47
+ : text;
48
+ const visible = request
49
+ .replace(IN_APP_BROWSER_CONTEXT_RE, ' ')
50
+ .replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, ' ')
51
+ .replace(/\[Image\s+#\d+\]/gi, ' ')
52
+ .replace(/\n{3,}/g, '\n\n')
53
+ .trim();
54
+ return visible || undefined;
55
+ }
56
+ export function codexUserMessageOrigin(text) {
57
+ const trimmed = text.trimStart();
58
+ if (CODEX_RUNTIME_MESSAGE_RE.test(trimmed))
59
+ return 'runtime';
60
+ if (/^<in-app-browser-context\b/i.test(trimmed) && !codexUserDisplayText(text))
61
+ return 'runtime';
62
+ return 'human';
63
+ }
64
+ /**
65
+ * Records consumed by correlation or retained only as source provenance do not
66
+ * emit a standalone Trace IR event. Keeping this inventory explicit preserves
67
+ * `unknown` as a forward-compatibility warning.
68
+ */
69
+ export function isCodexRecordConsumedWithoutDirectEvent(recordType, payloadType) {
70
+ if (recordType === 'world_state')
71
+ return true;
72
+ if (recordType === 'inter_agent_communication_metadata')
73
+ return true;
74
+ if (recordType !== 'event_msg')
75
+ return false;
76
+ return payloadType === 'web_search_end'
77
+ || payloadType === 'image_generation_end';
78
+ }
@@ -0,0 +1,16 @@
1
+ import type { TraceToolStatus } from './trace-ir.js';
2
+ export interface CodexToolOutcome {
3
+ status: TraceToolStatus;
4
+ present: boolean;
5
+ }
6
+ /** Normalize Codex status values before source adapters project Trace IR. */
7
+ export declare function codexToolStatusFromValue(value: unknown): TraceToolStatus;
8
+ /** Runtime end records are authoritative even when they only expose an Ok/Err envelope. */
9
+ export declare function codexRuntimeToolOutcome(end: {
10
+ status?: unknown;
11
+ isError?: boolean;
12
+ } | undefined): CodexToolOutcome;
13
+ /** Extract the same authoritative outcome from a raw Codex runtime-end payload. */
14
+ export declare function codexRuntimeToolOutcomeFromPayload(payloadType: string | undefined, payload: Record<string, unknown>): CodexToolOutcome;
15
+ /** Infer bridge output status only when Codex did not record an explicit status. */
16
+ export declare function codexToolOutputOutcome(output: unknown, explicitStatus?: unknown): CodexToolOutcome;