oh-my-knowledge 0.51.2 → 0.52.1

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 +2300 -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,324 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { basename, extname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { writeJsonFileAtomic } from '../shared/atomic-json.js';
5
+ import { normalizeTraceTimestamp } from './trace-ir.js';
6
+ import { forEachNonEmptyUtf8Line } from './trace-source.js';
7
+ const ARCHIVE_SCHEMA_VERSION = 1;
8
+ const MAX_ARCHIVE_SOURCE_BYTES = 16 * 1024 * 1024;
9
+ const MAX_ARCHIVE_FILE_BYTES = 64 * 1024 * 1024;
10
+ const MAX_RECORD_CHARS = 512 * 1024;
11
+ /**
12
+ * Persist bounded source records beside a report. Absolute source paths are
13
+ * consumed only during trusted ingest; Studio later reads relative sidecars.
14
+ */
15
+ export function writeObservationSourceRecordArchives(report, outDir, reportPath) {
16
+ const sessions = report.experience?.sessions ?? [];
17
+ if (sessions.length === 0)
18
+ return [];
19
+ const reportStem = basename(reportPath).replace(/\.report\.json$/u, '');
20
+ const archiveDir = join(outDir, 'source-records', reportStem);
21
+ const builders = sessions.map((session) => ({
22
+ session,
23
+ groupedRanges: groupRecordRanges(session.timelineScope.sessionRecordRanges),
24
+ records: [],
25
+ omittedRecordCount: 0,
26
+ byteCount: 0,
27
+ truncated: false,
28
+ }));
29
+ const targetsBySource = new Map();
30
+ for (const builder of builders) {
31
+ for (const [sourceTrace, ranges] of builder.groupedRanges) {
32
+ const targets = targetsBySource.get(sourceTrace) ?? [];
33
+ targets.push({
34
+ builder,
35
+ ranges,
36
+ expectedCount: ranges.reduce((sum, range) => sum + range.end - range.start + 1, 0),
37
+ matchedCount: 0,
38
+ rangeIndex: 0,
39
+ });
40
+ targetsBySource.set(sourceTrace, targets);
41
+ }
42
+ }
43
+ for (const [sourceTrace, targets] of targetsBySource) {
44
+ collectSourceRecords(sourceTrace, targets);
45
+ }
46
+ return builders.map((builder) => persistSessionArchive(builder, archiveDir, outDir, report.meta.generatedAt));
47
+ }
48
+ export function loadObservationSourceRecordArchive(ref, observationsDir) {
49
+ if (!ref || ref.status === 'unavailable' || !ref.relativePath) {
50
+ return unavailableView(ref?.reason ?? 'no_record_ranges');
51
+ }
52
+ const path = safeArchivePath(observationsDir, ref.relativePath);
53
+ if (!path || !existsSync(path))
54
+ return unavailableView('archive_invalid');
55
+ try {
56
+ if (statSync(path).size > MAX_ARCHIVE_FILE_BYTES)
57
+ return unavailableView('archive_invalid');
58
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
59
+ if (!isObservationSourceRecordArchive(parsed, ref.experienceSessionId)) {
60
+ return unavailableView('archive_invalid');
61
+ }
62
+ return {
63
+ status: ref.status,
64
+ recordCount: parsed.records.length,
65
+ records: parsed.records,
66
+ omittedRecordCount: parsed.omittedRecordCount,
67
+ byteCount: parsed.byteCount,
68
+ truncated: parsed.truncated,
69
+ ...(ref.reason ? { reason: ref.reason } : {}),
70
+ };
71
+ }
72
+ catch {
73
+ return unavailableView('archive_invalid');
74
+ }
75
+ }
76
+ /**
77
+ * Build the page-safe archive metadata without reading the potentially large
78
+ * sidecar. The source-record endpoint resolves the same ref on demand.
79
+ */
80
+ export function summarizeObservationSourceRecordArchive(ref) {
81
+ if (!ref)
82
+ return unavailableView('no_record_ranges');
83
+ return {
84
+ status: ref.status,
85
+ recordCount: ref.recordCount,
86
+ records: [],
87
+ omittedRecordCount: ref.omittedRecordCount,
88
+ byteCount: ref.byteCount,
89
+ truncated: ref.truncated,
90
+ ...(ref.reason ? { reason: ref.reason } : {}),
91
+ };
92
+ }
93
+ function collectSourceRecords(sourceTrace, targets) {
94
+ if (extname(sourceTrace).toLowerCase() !== '.jsonl') {
95
+ markSourceUnavailable(targets, 'unsupported_source');
96
+ return;
97
+ }
98
+ if (!existsSync(sourceTrace)) {
99
+ markSourceUnavailable(targets, 'source_missing');
100
+ return;
101
+ }
102
+ const maxEnd = targets.reduce((targetMax, target) => target.ranges.reduce((rangeMax, range) => Math.max(rangeMax, range.end), targetMax), -1);
103
+ let sourceIndex = 0;
104
+ try {
105
+ forEachNonEmptyUtf8Line(sourceTrace, (line) => {
106
+ const currentIndex = sourceIndex;
107
+ sourceIndex += 1;
108
+ const recordsByTraceId = new Map();
109
+ for (const target of targets) {
110
+ while (target.ranges[target.rangeIndex]
111
+ && target.ranges[target.rangeIndex].end < currentIndex) {
112
+ target.rangeIndex += 1;
113
+ }
114
+ const range = target.ranges[target.rangeIndex];
115
+ if (!range || currentIndex < range.start || currentIndex > range.end)
116
+ continue;
117
+ target.matchedCount += 1;
118
+ if (target.builder.byteCount >= MAX_ARCHIVE_SOURCE_BYTES) {
119
+ target.builder.omittedRecordCount += 1;
120
+ target.builder.truncated = true;
121
+ target.builder.failure = 'archive_limit';
122
+ continue;
123
+ }
124
+ let record = recordsByTraceId.get(range.traceId);
125
+ if (!record) {
126
+ record = observationSourceRecordFromLine(line, currentIndex, range.traceId, sourceTrace);
127
+ recordsByTraceId.set(range.traceId, record);
128
+ }
129
+ if (target.builder.byteCount + record.byteCount > MAX_ARCHIVE_SOURCE_BYTES) {
130
+ target.builder.omittedRecordCount += 1;
131
+ target.builder.truncated = true;
132
+ target.builder.failure = 'archive_limit';
133
+ continue;
134
+ }
135
+ target.builder.records.push(record);
136
+ target.builder.byteCount += record.byteCount;
137
+ target.builder.truncated ||= record.truncated;
138
+ }
139
+ return currentIndex < maxEnd;
140
+ });
141
+ }
142
+ catch {
143
+ for (const target of targets)
144
+ target.builder.failure ??= 'read_failed';
145
+ }
146
+ for (const target of targets) {
147
+ target.builder.omittedRecordCount += Math.max(0, target.expectedCount - target.matchedCount);
148
+ }
149
+ }
150
+ function markSourceUnavailable(targets, reason) {
151
+ for (const target of targets) {
152
+ target.builder.omittedRecordCount += target.expectedCount;
153
+ target.builder.failure ??= reason;
154
+ }
155
+ }
156
+ function persistSessionArchive(builder, archiveDir, outDir, generatedAt) {
157
+ const { session, records, omittedRecordCount, byteCount, failure } = builder;
158
+ if (builder.groupedRanges.size === 0)
159
+ return unavailableRef(session.id, 'no_record_ranges');
160
+ if (records.length === 0)
161
+ return unavailableRef(session.id, failure ?? 'read_failed', omittedRecordCount);
162
+ const archive = {
163
+ archiveKind: 'observe-source-records',
164
+ schemaVersion: ARCHIVE_SCHEMA_VERSION,
165
+ experienceSessionId: session.id,
166
+ generatedAt,
167
+ records,
168
+ omittedRecordCount,
169
+ byteCount,
170
+ truncated: builder.truncated || omittedRecordCount > 0,
171
+ };
172
+ const fileName = `${createHash('sha256').update(session.id).digest('hex').slice(0, 24)}.json`;
173
+ const archivePath = join(archiveDir, fileName);
174
+ writeJsonFileAtomic(archivePath, archive);
175
+ const relativePath = relative(outDir, archivePath).split('\\').join('/');
176
+ const partial = archive.truncated || failure !== undefined;
177
+ return {
178
+ experienceSessionId: session.id,
179
+ status: partial ? 'partial' : 'available',
180
+ relativePath,
181
+ recordCount: records.length,
182
+ omittedRecordCount,
183
+ byteCount,
184
+ truncated: archive.truncated,
185
+ ...(failure ? { reason: failure } : {}),
186
+ };
187
+ }
188
+ export function observationSourceRecordFromLine(line, sourceIndex, traceId, sourceTrace) {
189
+ let record = {};
190
+ let serialized = line;
191
+ let redacted = false;
192
+ try {
193
+ const parsed = JSON.parse(line);
194
+ if (isRecord(parsed)) {
195
+ record = parsed;
196
+ serialized = JSON.stringify(parsed, (key, value) => {
197
+ if (key !== 'encrypted_content')
198
+ return value;
199
+ redacted = true;
200
+ return '[opaque encrypted content omitted]';
201
+ });
202
+ }
203
+ }
204
+ catch {
205
+ // Malformed source lines are still useful provenance.
206
+ }
207
+ const payload = isRecord(record.payload) ? record.payload : {};
208
+ const recordType = textValue(record.type) ?? 'unknown';
209
+ const payloadType = textValue(payload.type);
210
+ const raw = serialized.length > MAX_RECORD_CHARS ? serialized.slice(0, MAX_RECORD_CHARS) : serialized;
211
+ return {
212
+ sourceIndex,
213
+ traceId,
214
+ sourceTrace,
215
+ sourceType: `${recordType}:${payloadType ?? ''}`,
216
+ sourceEventId: textValue(payload.id),
217
+ timestamp: normalizeTraceTimestamp(record.timestamp),
218
+ raw,
219
+ byteCount: Buffer.byteLength(raw),
220
+ truncated: raw.length < serialized.length,
221
+ redacted,
222
+ };
223
+ }
224
+ function groupRecordRanges(ranges) {
225
+ const grouped = new Map();
226
+ for (const range of ranges) {
227
+ const current = grouped.get(range.sourceTrace) ?? [];
228
+ current.push({ start: range.startRecordIndex, end: range.endRecordIndex, traceId: range.traceId });
229
+ grouped.set(range.sourceTrace, current);
230
+ }
231
+ for (const [sourceTrace, sourceRanges] of grouped) {
232
+ const sorted = sourceRanges.sort((a, b) => a.start - b.start || a.end - b.end);
233
+ const merged = [];
234
+ for (const range of sorted) {
235
+ const previous = merged.at(-1);
236
+ if (previous && previous.traceId === range.traceId && range.start <= previous.end + 1) {
237
+ previous.end = Math.max(previous.end, range.end);
238
+ }
239
+ else {
240
+ merged.push({ ...range });
241
+ }
242
+ }
243
+ grouped.set(sourceTrace, merged);
244
+ }
245
+ return grouped;
246
+ }
247
+ function safeArchivePath(observationsDir, relativePath) {
248
+ if (!relativePath || isAbsolute(relativePath))
249
+ return undefined;
250
+ const candidate = resolve(observationsDir, relativePath);
251
+ const lexicalRelative = relative(resolve(observationsDir), candidate);
252
+ if (lexicalRelative.startsWith('..') || isAbsolute(lexicalRelative))
253
+ return undefined;
254
+ try {
255
+ const root = realpathSync(observationsDir);
256
+ const resolved = realpathSync(candidate);
257
+ const resolvedRelative = relative(root, resolved);
258
+ if (resolvedRelative.startsWith('..') || isAbsolute(resolvedRelative))
259
+ return undefined;
260
+ return resolved;
261
+ }
262
+ catch {
263
+ return undefined;
264
+ }
265
+ }
266
+ function unavailableRef(experienceSessionId, reason, omittedRecordCount = 0) {
267
+ return {
268
+ experienceSessionId,
269
+ status: 'unavailable',
270
+ recordCount: 0,
271
+ omittedRecordCount,
272
+ byteCount: 0,
273
+ truncated: omittedRecordCount > 0,
274
+ reason,
275
+ };
276
+ }
277
+ function unavailableView(reason) {
278
+ return {
279
+ status: 'unavailable',
280
+ recordCount: 0,
281
+ records: [],
282
+ omittedRecordCount: 0,
283
+ byteCount: 0,
284
+ truncated: false,
285
+ reason,
286
+ };
287
+ }
288
+ function isObservationSourceRecordArchive(value, experienceSessionId) {
289
+ if (!isRecord(value)
290
+ || value.archiveKind !== 'observe-source-records'
291
+ || value.schemaVersion !== ARCHIVE_SCHEMA_VERSION
292
+ || value.experienceSessionId !== experienceSessionId
293
+ || typeof value.generatedAt !== 'string'
294
+ || !Array.isArray(value.records)
295
+ || !value.records.every(isObservationSourceRecord)
296
+ || !isCount(value.omittedRecordCount)
297
+ || !isCount(value.byteCount)
298
+ || typeof value.truncated !== 'boolean')
299
+ return false;
300
+ return value.records.reduce((sum, record) => sum + record.byteCount, 0) === value.byteCount;
301
+ }
302
+ function isObservationSourceRecord(value) {
303
+ return isRecord(value)
304
+ && isCount(value.sourceIndex)
305
+ && typeof value.traceId === 'string'
306
+ && typeof value.sourceTrace === 'string'
307
+ && typeof value.sourceType === 'string'
308
+ && (value.sourceEventId === undefined || typeof value.sourceEventId === 'string')
309
+ && (value.timestamp === undefined || typeof value.timestamp === 'string')
310
+ && typeof value.raw === 'string'
311
+ && isCount(value.byteCount)
312
+ && Buffer.byteLength(value.raw) === value.byteCount
313
+ && typeof value.truncated === 'boolean'
314
+ && typeof value.redacted === 'boolean';
315
+ }
316
+ function isRecord(value) {
317
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
318
+ }
319
+ function isCount(value) {
320
+ return Number.isSafeInteger(value) && Number(value) >= 0;
321
+ }
322
+ function textValue(value) {
323
+ return typeof value === 'string' && value.trim() ? value : undefined;
324
+ }
@@ -0,0 +1,8 @@
1
+ import type { ExperienceTimelineEvent, TaskTrajectorySession, TaskWindowScope } from '../types/index.js';
2
+ export declare const TASK_SEMANTIC_EVENT_LIMIT = 240;
3
+ export interface ResolvedTaskWindow {
4
+ events: ExperienceTimelineEvent[];
5
+ semanticEvents: ExperienceTimelineEvent[];
6
+ scope: TaskWindowScope;
7
+ }
8
+ export declare function resolveTaskWindow(session: TaskTrajectorySession, targetTurnId: string, semanticLimit?: number): ResolvedTaskWindow;
@@ -0,0 +1,36 @@
1
+ export const TASK_SEMANTIC_EVENT_LIMIT = 240;
2
+ export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SEMANTIC_EVENT_LIMIT) {
3
+ const events = session.fullSessionTimeline;
4
+ const attributedIds = new Set(session.attributedEventIds);
5
+ const attributed = events.filter((event) => attributedIds.has(event.id));
6
+ const selected = session.turns.find((turn) => turn.turnId === targetTurnId);
7
+ const eventById = new Map(events.map((event) => [event.id, event]));
8
+ const taskEvents = selected
9
+ ? selected.eventIds.flatMap((id) => {
10
+ const event = eventById.get(id);
11
+ return event ? [event] : [];
12
+ })
13
+ : [];
14
+ const semanticEvents = semanticPreview(taskEvents, semanticLimit);
15
+ const matchedAttributedEventCount = taskEvents.filter((event) => attributedIds.has(event.id)).length;
16
+ return {
17
+ events: taskEvents,
18
+ semanticEvents,
19
+ scope: {
20
+ basis: selected?.boundaryBasis ?? 'unresolved',
21
+ turnId: selected?.turnId,
22
+ normalizedEventCount: taskEvents.length,
23
+ semanticEventCount: semanticEvents.length,
24
+ attributedEventCount: attributed.length,
25
+ matchedAttributedEventCount,
26
+ truncated: semanticEvents.length < taskEvents.length,
27
+ },
28
+ };
29
+ }
30
+ function semanticPreview(events, limit) {
31
+ if (events.length <= limit)
32
+ return events;
33
+ const headCount = Math.ceil(limit * 0.6);
34
+ const tailCount = limit - headCount;
35
+ return [...events.slice(0, headCount), ...events.slice(-tailCount)];
36
+ }
@@ -7,6 +7,11 @@ export type TraceMessageRole = 'user' | 'assistant' | 'system';
7
7
  export type TraceMessageOrigin = 'human' | 'runtime' | 'skill-context' | 'synthetic';
8
8
  export type TraceToolStatus = ToolCallStatus;
9
9
  export type TraceToolStatusSource = ToolCallStatusSource;
10
+ export type TraceModelActivityKind = 'reasoning';
11
+ export type TraceModelActivityVisibility = 'plaintext' | 'opaque';
12
+ export type TraceModelActivityContentSource = 'summary' | 'content' | 'text';
13
+ export type TraceRuntimeContextKind = 'session_context' | 'execution_context' | 'settings' | 'goal';
14
+ export type TraceAgentActivityKind = 'communication' | 'status';
10
15
  /** Source-neutral tool identity, including provider namespaces when present. */
11
16
  export type TraceToolRef = NormalizedToolIdentity;
12
17
  interface TraceEventBase {
@@ -23,6 +28,8 @@ export interface TraceMessageEvent extends TraceEventBase {
23
28
  role: TraceMessageRole;
24
29
  origin: TraceMessageOrigin;
25
30
  text: string;
31
+ /** Human-facing text after a source adapter removes transport/UI envelopes. */
32
+ displayText?: string;
26
33
  model?: string;
27
34
  attributionSkill?: string;
28
35
  }
@@ -51,17 +58,75 @@ export interface TraceUsageEvent extends TraceEventBase {
51
58
  cacheCreationTokens: number;
52
59
  reasoningTokens?: number;
53
60
  }
61
+ export interface TraceModelActivityEvent extends TraceEventBase {
62
+ eventKind: 'model_activity';
63
+ activityKind: TraceModelActivityKind;
64
+ contentVisibility: TraceModelActivityVisibility;
65
+ text?: string;
66
+ contentSource?: TraceModelActivityContentSource;
67
+ model?: string;
68
+ }
54
69
  export interface TraceLifecycleEvent extends TraceEventBase {
55
70
  eventKind: 'lifecycle';
56
71
  phase: 'session_started' | 'session_ended' | 'turn_started' | 'turn_completed' | 'turn_aborted' | 'turn_interrupted';
57
72
  reason?: string;
58
73
  durationMs?: number;
59
74
  }
75
+ /** Source-neutral execution context visible to the agent for a turn or run. */
76
+ export interface TraceRuntimeContextEvent extends TraceEventBase {
77
+ eventKind: 'runtime_context';
78
+ runtimeKind: TraceRuntimeContextKind;
79
+ runtimeName?: string;
80
+ runtimeVersion?: string;
81
+ cwd?: string;
82
+ workspaceRoots?: string[];
83
+ currentDate?: string;
84
+ timezone?: string;
85
+ model?: string;
86
+ modelProvider?: string;
87
+ serviceTier?: string;
88
+ reasoningEffort?: string;
89
+ reasoningSummary?: string;
90
+ personality?: string;
91
+ approvalPolicy?: string;
92
+ approvalReviewer?: string;
93
+ permissionProfile?: string;
94
+ sandboxMode?: string;
95
+ collaborationMode?: string;
96
+ realtimeActive?: boolean;
97
+ multiAgentMode?: string;
98
+ multiAgentVersion?: string;
99
+ memoryMode?: string;
100
+ historyMode?: string;
101
+ contextWindowId?: string;
102
+ availableTools?: string[];
103
+ instructions?: string;
104
+ goal?: string;
105
+ goalStatus?: string;
106
+ summary?: string;
107
+ }
108
+ /** A source-reported context-window compaction boundary. */
109
+ export interface TraceContextCompactionEvent extends TraceEventBase {
110
+ eventKind: 'context_compaction';
111
+ summary?: string;
112
+ replacementItemCount?: number;
113
+ }
114
+ /** Observable communication or status emitted by a cooperating agent. */
115
+ export interface TraceAgentActivityEvent extends TraceEventBase {
116
+ eventKind: 'agent_activity';
117
+ activityKind: TraceAgentActivityKind;
118
+ agentId?: string;
119
+ agentPath?: string;
120
+ activity?: string;
121
+ author?: string;
122
+ recipient?: string;
123
+ text?: string;
124
+ }
60
125
  export interface TraceUnknownEvent extends TraceEventBase {
61
126
  eventKind: 'unknown';
62
127
  raw?: unknown;
63
128
  }
64
- export type TraceEvent = TraceMessageEvent | TraceToolCallEvent | TraceToolResultEvent | TraceUsageEvent | TraceLifecycleEvent | TraceUnknownEvent;
129
+ export type TraceEvent = TraceMessageEvent | TraceToolCallEvent | TraceToolResultEvent | TraceUsageEvent | TraceModelActivityEvent | TraceLifecycleEvent | TraceRuntimeContextEvent | TraceContextCompactionEvent | TraceAgentActivityEvent | TraceUnknownEvent;
65
130
  export interface TraceSession {
66
131
  /** Concrete source run/thread identifier. */
67
132
  runId: string;
@@ -96,4 +96,5 @@ export declare function loadTraceSessions(path: string): TraceSession[];
96
96
  export declare function loadTraceCorpus(path: string): TraceCorpus;
97
97
  /** @deprecated Use `loadTraceSessions`. */
98
98
  export declare const loadCcSessions: typeof loadTraceSessions;
99
+ export declare function forEachNonEmptyUtf8Line(filePath: string, visit: (trimmedLine: string) => boolean | void): void;
99
100
  export declare function legacyCcSessionToTraceSession(session: CcSession): TraceSession;
@@ -333,7 +333,7 @@ function parseJsonlSessionFile(filePath) {
333
333
  },
334
334
  };
335
335
  }
336
- function forEachNonEmptyUtf8Line(filePath, visit) {
336
+ export function forEachNonEmptyUtf8Line(filePath, visit) {
337
337
  let fd;
338
338
  try {
339
339
  fd = openSync(filePath, 'r');
@@ -344,30 +344,37 @@ function forEachNonEmptyUtf8Line(filePath, visit) {
344
344
  const decoder = new StringDecoder('utf8');
345
345
  const buffer = Buffer.allocUnsafe(TRACE_READ_CHUNK_BYTES);
346
346
  let pending = '';
347
+ let stopped = false;
347
348
  const consumeCompleteLines = () => {
348
349
  let newline = pending.indexOf('\n');
349
- while (newline >= 0) {
350
+ while (newline >= 0 && !stopped) {
350
351
  if (newline > MAX_JSONL_RECORD_CHARS) {
351
352
  throw new Error(`trace JSONL 单条记录超过 ${MAX_JSONL_RECORD_CHARS} 字符上限:${filePath}`);
352
353
  }
353
354
  const trimmed = pending.slice(0, newline).trim();
354
355
  pending = pending.slice(newline + 1);
355
- if (trimmed)
356
- visit(trimmed);
356
+ if (trimmed && visit(trimmed) === false) {
357
+ stopped = true;
358
+ return;
359
+ }
357
360
  newline = pending.indexOf('\n');
358
361
  }
362
+ if (stopped)
363
+ return;
359
364
  if (pending.length > MAX_JSONL_RECORD_CHARS) {
360
365
  throw new Error(`trace JSONL 单条记录超过 ${MAX_JSONL_RECORD_CHARS} 字符上限:${filePath}`);
361
366
  }
362
367
  };
363
368
  try {
364
- while (true) {
369
+ while (!stopped) {
365
370
  const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
366
371
  if (bytesRead === 0)
367
372
  break;
368
373
  pending += decoder.write(buffer.subarray(0, bytesRead));
369
374
  consumeCompleteLines();
370
375
  }
376
+ if (stopped)
377
+ return;
371
378
  pending += decoder.end();
372
379
  consumeCompleteLines();
373
380
  const trimmed = pending.trim();
@@ -0,0 +1,3 @@
1
+ import type { ExperienceTimelineEvent, ExperienceTurnSummary } from '../types/index.js';
2
+ /** Reconstruct every observable task without consulting Skill attribution. */
3
+ export declare function reconstructExperienceTurns(timeline: ExperienceTimelineEvent[]): ExperienceTurnSummary[];