oh-my-knowledge 0.52.1 → 0.52.2

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.
@@ -51,6 +51,10 @@ export interface CodexJsonlLine {
51
51
  startOffset: number;
52
52
  endOffset: number;
53
53
  }
54
+ export interface CodexTaskRecordReadOptions {
55
+ /** Include only the next task prefix through its first human user message. */
56
+ includeNextHumanMessage?: boolean;
57
+ }
54
58
  /**
55
59
  * Build a compact byte-range index without retaining the rollout. Only records
56
60
  * needed for turn boundaries and list metrics are parsed.
@@ -80,7 +84,7 @@ export declare function isCurrentCodexRolloutIndex(value: unknown, sourcePath: s
80
84
  * index remains internally consistent even after newer records are appended.
81
85
  */
82
86
  export declare function isReusableCodexRolloutIndex(value: unknown, sourcePath: string): value is CodexRolloutIndex;
83
- export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask): {
87
+ export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask, options?: CodexTaskRecordReadOptions): {
84
88
  records: Array<unknown | undefined>;
85
89
  lines: CodexJsonlLine[];
86
90
  malformedRecordCount: number;
@@ -327,7 +327,7 @@ export function isReusableCodexRolloutIndex(value, sourcePath) {
327
327
  return false;
328
328
  }
329
329
  }
330
- export function readCodexTaskRecords(index, task) {
330
+ export function readCodexTaskRecords(index, task, options = {}) {
331
331
  const records = [];
332
332
  if (index.sessionMeta !== undefined && index.sessionMetaLine !== undefined) {
333
333
  records[index.sessionMetaLine] = index.sessionMeta;
@@ -345,8 +345,40 @@ export function readCodexTaskRecords(index, task) {
345
345
  malformedRecordCount += 1;
346
346
  }
347
347
  }, task.startOffset, task.endOffset, task.startLine);
348
+ if (options.includeNextHumanMessage) {
349
+ const taskIndex = index.tasks.findIndex((candidate) => candidate.turnId === task.turnId);
350
+ const nextTask = taskIndex >= 0 ? index.tasks[taskIndex + 1] : undefined;
351
+ if (nextTask) {
352
+ forEachJsonlLine(index.sourcePath, (record) => {
353
+ lines.push(record);
354
+ try {
355
+ const parsed = JSON.parse(record.text);
356
+ records[record.line] = parsed;
357
+ return isHumanUserPrompt(parsed) ? false : undefined;
358
+ }
359
+ catch {
360
+ malformedRecordCount += 1;
361
+ return undefined;
362
+ }
363
+ }, nextTask.startOffset, nextTask.endOffset, nextTask.startLine);
364
+ }
365
+ }
348
366
  return { records, lines, malformedRecordCount };
349
367
  }
368
+ function isHumanUserPrompt(value) {
369
+ const raw = objectValue(value);
370
+ if (!raw)
371
+ return false;
372
+ const recordType = stringValue(raw.type);
373
+ const payload = objectValue(raw.payload) ?? {};
374
+ const payloadType = stringValue(payload.type);
375
+ if (!isUserPromptRecord(recordType, payloadType, payload))
376
+ return false;
377
+ const message = userPromptText(payload)?.trim();
378
+ return Boolean(message
379
+ && codexUserMessageOrigin(message) === 'human'
380
+ && codexUserDisplayText(message)?.trim());
381
+ }
350
382
  function finalizeSupersededTask(task, endOffset, endLine, endTimestamp) {
351
383
  return stripMutable({
352
384
  ...task,
@@ -501,7 +533,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
501
533
  let indexedEndsWithNewline = true;
502
534
  let fragments = [];
503
535
  try {
504
- while (absoluteOffset < endOffset) {
536
+ readLoop: while (absoluteOffset < endOffset) {
505
537
  const requestBytes = Math.min(buffer.length, endOffset - absoluteOffset);
506
538
  const bytesRead = readSync(fd, buffer, 0, requestBytes, absoluteOffset);
507
539
  if (bytesRead === 0)
@@ -515,7 +547,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
515
547
  ? Buffer.concat([...fragments, tail])
516
548
  : tail;
517
549
  const nextOffset = absoluteOffset + index + 1;
518
- emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
550
+ const shouldContinue = emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
519
551
  indexedSize = nextOffset;
520
552
  indexedLineCount = lineNumber + 1;
521
553
  indexedEndsWithNewline = true;
@@ -523,6 +555,8 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
523
555
  cursor = index + 1;
524
556
  lineStartOffset = nextOffset;
525
557
  lineNumber += 1;
558
+ if (!shouldContinue)
559
+ break readLoop;
526
560
  }
527
561
  if (cursor < bytesRead)
528
562
  fragments.push(Buffer.from(buffer.subarray(cursor, bytesRead)));
@@ -561,6 +595,6 @@ function isCompleteJsonRecord(buffer) {
561
595
  function emitLine(buffer, line, startOffset, endOffset, visit) {
562
596
  const text = buffer.toString('utf8').trim();
563
597
  if (!text)
564
- return;
565
- visit({ text, line, startOffset, endOffset });
598
+ return true;
599
+ return visit({ text, line, startOffset, endOffset }) !== false;
566
600
  }
@@ -106,7 +106,9 @@ class CodexConversationCatalog {
106
106
  trajectoryFromIndex(row, index, indexedTask) {
107
107
  const threadId = row.id;
108
108
  const turnId = indexedTask.turnId;
109
- const selected = readCodexTaskRecords(index, indexedTask);
109
+ const selected = readCodexTaskRecords(index, indexedTask, {
110
+ includeNextHumanMessage: true,
111
+ });
110
112
  const traceSession = parseCodexSessionFile(row.rolloutPath, selected.records);
111
113
  const fullSessionTimeline = projectTraceSessionTimeline(traceSession);
112
114
  const turns = reconstructExperienceTurns(fullSessionTimeline);
@@ -28,6 +28,11 @@ export interface TextMatchRange {
28
28
  }
29
29
  export declare function findUserCorrectionMatches(value: string): TextMatchRange[];
30
30
  export declare function hasUserCorrectionSignal(value: string): boolean;
31
+ /**
32
+ * 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
33
+ * 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
34
+ */
35
+ export declare function hasExplicitFollowUpCorrectionSignal(value: string): boolean;
31
36
  export declare function findUserGoalShiftMatches(value: string): TextMatchRange[];
32
37
  export declare function hasUserGoalShiftSignal(value: string): boolean;
33
38
  export declare function findNegativeFeedbackMatches(value: string): TextMatchRange[];
@@ -40,6 +40,14 @@ const PHRASE_USER_CORRECTION_TERMS = [
40
40
  '改成',
41
41
  '重来',
42
42
  ];
43
+ const EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS = [
44
+ /(?:^|[\s,,;;.。!!??::])(?:不对|错了)(?:啊|呀|吧|呢)?(?=$|[\s,,;;.。!!??::])/iu,
45
+ /(?:这|这个|这样|你的|你.{0,8})(?:不对|错了)/iu,
46
+ /不是这个|不是我要的|不要这样|理解错|看错|你没懂|重来|重新来|重做|重新做/iu,
47
+ /\b(?:that(?:'s| is)|this is|you(?:'re| are)) wrong\b/iu,
48
+ /\b(?:this|that) is (?:incorrect|not right)\b/iu,
49
+ /\byou (?:misunderstood|misread)\b|\b(?:start|do)(?: it)? over\b/iu,
50
+ ];
43
51
  const USER_GOAL_SHIFT_TERMS = [
44
52
  '换个方向',
45
53
  '重新来',
@@ -165,6 +173,13 @@ export function findUserCorrectionMatches(value) {
165
173
  export function hasUserCorrectionSignal(value) {
166
174
  return findUserCorrectionMatches(value).length > 0;
167
175
  }
176
+ /**
177
+ * 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
178
+ * 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
179
+ */
180
+ export function hasExplicitFollowUpCorrectionSignal(value) {
181
+ return EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS.some((pattern) => pattern.test(value));
182
+ }
168
183
  export function findUserGoalShiftMatches(value) {
169
184
  const ranges = [];
170
185
  for (const term of USER_GOAL_SHIFT_TERMS) {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { hasUserCorrectionSignal } from './feedback-matchers.js';
2
3
  import { resolveTaskWindow } from './task-window.js';
3
4
  const AGENTS_CONTEXT_RE = /^# AGENTS\.md instructions for ([^\n]+)\n/gim;
4
5
  const SKILL_PATH_RE = /((?:~|\.{0,2}|\/)?[^\s"'`]*\/skills\/(?:\.system\/)?([^/\s"'`]+)\/SKILL\.md)\b/i;
@@ -16,13 +17,16 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
16
17
  }) {
17
18
  const taskWindow = resolveTaskWindow(session, targetTurnId);
18
19
  const normalizedEvents = taskWindow.events;
20
+ const normalizedTimeline = normalizedEvents.filter((event) => event.runtimeKind !== 'usage');
19
21
  const timeline = taskWindow.semanticEvents.filter((event) => event.runtimeKind !== 'usage');
22
+ const replayTimeline = [...timeline, ...taskWindow.relatedEvents]
23
+ .sort((left, right) => left.order - right.order);
20
24
  const knowledgeEvidence = projectKnowledgeEvidence(timeline);
21
- const steps = buildTaskReplaySteps(session, timeline, knowledgeEvidence);
22
- const notices = buildIntegrityNotices(taskWindow.scope, timeline, steps, ingestion);
23
- const userEvents = timeline.filter((event) => event.kind === 'user_message');
24
- const assistantEvents = timeline.filter((event) => event.kind === 'assistant_message');
25
- const observedModels = timeline.reduce((models, event) => {
25
+ const steps = buildTaskReplaySteps(session, replayTimeline, knowledgeEvidence);
26
+ const notices = buildIntegrityNotices(taskWindow.scope, normalizedEvents, ingestion);
27
+ const userEvents = normalizedTimeline.filter((event) => event.kind === 'user_message');
28
+ const assistantEvents = normalizedTimeline.filter((event) => event.kind === 'assistant_message');
29
+ const observedModels = normalizedTimeline.reduce((models, event) => {
26
30
  const eventModel = event.model?.trim();
27
31
  if (eventModel && !models.includes(eventModel))
28
32
  models.push(eventModel);
@@ -34,11 +38,11 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
34
38
  summary: {
35
39
  userGoal: eventText(userEvents[0]),
36
40
  finalResponse: eventText(assistantEvents.at(-1)),
37
- observedStartTimestamp: timeline.find((event) => (event.timestamp && event.runtimeKind !== 'session_context'))?.timestamp
38
- ?? timeline.find((event) => event.timestamp)?.timestamp,
39
- observedEndTimestamp: [...timeline].reverse().find((event) => event.timestamp)?.timestamp,
40
- toolCallCount: steps.filter((step) => step.stepKind === 'tool_exchange').length,
41
- toolFailureCount: steps.filter((step) => step.stepKind === 'tool_exchange' && step.toolStatus === 'failure').length,
41
+ observedStartTimestamp: normalizedTimeline.find((event) => (event.timestamp && event.runtimeKind !== 'session_context'))?.timestamp
42
+ ?? normalizedTimeline.find((event) => event.timestamp)?.timestamp,
43
+ observedEndTimestamp: [...normalizedTimeline].reverse().find((event) => event.timestamp)?.timestamp,
44
+ toolCallCount: normalizedEvents.filter((event) => event.kind === 'tool_use').length,
45
+ toolFailureCount: normalizedEvents.filter((event) => (event.kind === 'tool_result' && (event.toolStatus === 'failure' || event.isError))).length,
42
46
  hasUserCorrection: steps.some((step) => step.stepKind === 'user_correction'),
43
47
  observedModels,
44
48
  },
@@ -199,7 +203,7 @@ function isKnowledgeReturningCall(toolName, callText) {
199
203
  const command = sourceLocator(callText);
200
204
  return command ? READ_ONLY_SHELL_RE.test(command) : false;
201
205
  }
202
- function buildIntegrityNotices(taskScope, timeline, steps, ingestion) {
206
+ function buildIntegrityNotices(taskScope, normalizedEvents, ingestion) {
203
207
  const notices = [];
204
208
  if (taskScope.basis === 'unresolved') {
205
209
  notices.push({ code: 'task_boundary_unavailable', count: 1 });
@@ -214,17 +218,40 @@ function buildIntegrityNotices(taskScope, timeline, steps, ingestion) {
214
218
  notices.push({ code: 'ignored_values', count: ingestion.ignoredValueCount });
215
219
  if (ingestion?.unknownEventCount)
216
220
  notices.push({ code: 'unknown_events', count: ingestion.unknownEventCount });
217
- const unmatchedCalls = steps.filter((step) => step.stepKind === 'tool_exchange' && step.events.length === 1).length;
218
- if (unmatchedCalls > 0)
219
- notices.push({ code: 'unmatched_tool_calls', count: unmatchedCalls });
220
- const unmatchedResults = steps.filter((step) => step.stepKind === 'unmatched_tool_result').length;
221
- if (unmatchedResults > 0)
222
- notices.push({ code: 'unmatched_tool_results', count: unmatchedResults });
223
- const missingTimestamps = timeline.filter((event) => !event.timestamp).length;
221
+ const unmatched = unmatchedToolEventCounts(normalizedEvents);
222
+ if (unmatched.calls > 0)
223
+ notices.push({ code: 'unmatched_tool_calls', count: unmatched.calls });
224
+ if (unmatched.results > 0)
225
+ notices.push({ code: 'unmatched_tool_results', count: unmatched.results });
226
+ const missingTimestamps = normalizedEvents.filter((event) => !event.timestamp).length;
224
227
  if (missingTimestamps > 0)
225
228
  notices.push({ code: 'missing_timestamps', count: missingTimestamps });
226
229
  return notices;
227
230
  }
231
+ function unmatchedToolEventCounts(events) {
232
+ const availableResults = new Map();
233
+ for (const event of events) {
234
+ if (event.kind !== 'tool_result')
235
+ continue;
236
+ const key = toolCorrelationKey(event);
237
+ availableResults.set(key, (availableResults.get(key) ?? 0) + 1);
238
+ }
239
+ let unmatchedCalls = 0;
240
+ for (const event of events) {
241
+ if (event.kind !== 'tool_use')
242
+ continue;
243
+ const key = toolCorrelationKey(event);
244
+ const remaining = availableResults.get(key) ?? 0;
245
+ if (remaining === 0) {
246
+ unmatchedCalls += 1;
247
+ }
248
+ else {
249
+ availableResults.set(key, remaining - 1);
250
+ }
251
+ }
252
+ const unmatchedResults = [...availableResults.values()].reduce((sum, count) => sum + count, 0);
253
+ return { calls: unmatchedCalls, results: unmatchedResults };
254
+ }
228
255
  function correctionEventIds(session, timeline) {
229
256
  const ids = new Set();
230
257
  for (const episode of session.sessionStory?.episodes ?? []) {
@@ -235,9 +262,15 @@ function correctionEventIds(session, timeline) {
235
262
  }
236
263
  }
237
264
  const userCorrectionCount = session.indicators?.userCorrectionCount ?? 0;
265
+ const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
266
+ const explicitCandidates = timeline
267
+ .filter((event) => (event.kind === 'user_message'
268
+ && event.order > lastAssistantOrder
269
+ && hasUserCorrectionSignal(event.fullText ?? event.snippet ?? '')));
270
+ for (const event of explicitCandidates)
271
+ ids.add(event.id);
238
272
  if (ids.size > 0 || userCorrectionCount === 0)
239
273
  return ids;
240
- const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
241
274
  const correctionCandidates = timeline
242
275
  .filter((event) => event.kind === 'user_message' && event.order > lastAssistantOrder)
243
276
  .slice(-userCorrectionCount);
@@ -0,0 +1,11 @@
1
+ import type { ExperienceTimelineEvent } from '../types/index.js';
2
+ export interface TaskSemanticProjectionOptions {
3
+ preservePendingToolCalls?: boolean;
4
+ }
5
+ /**
6
+ * Bound one task's semantic projection without separating tool exchanges or
7
+ * blindly discarding the middle of a long task. Failures, task boundaries and
8
+ * the user/assistant endpoints are retained first; remaining capacity is
9
+ * distributed across the task so the preview still explains its progression.
10
+ */
11
+ export declare function projectTaskSemanticEvents(events: ExperienceTimelineEvent[], limit: number, options?: TaskSemanticProjectionOptions): ExperienceTimelineEvent[];
@@ -0,0 +1,172 @@
1
+ const PRIORITY_BANDS = [100, 90, 80, 70, 60, 40, 20, 0];
2
+ /**
3
+ * Bound one task's semantic projection without separating tool exchanges or
4
+ * blindly discarding the middle of a long task. Failures, task boundaries and
5
+ * the user/assistant endpoints are retained first; remaining capacity is
6
+ * distributed across the task so the preview still explains its progression.
7
+ */
8
+ export function projectTaskSemanticEvents(events, limit, options = {}) {
9
+ if (limit <= 0 || events.length === 0)
10
+ return [];
11
+ if (events.length <= limit)
12
+ return events;
13
+ const units = semanticUnits(events, options);
14
+ const selected = new Map();
15
+ let remaining = limit;
16
+ const required = units
17
+ .filter((unit) => unit.required)
18
+ .sort((left, right) => right.priority - left.priority || left.retentionOrder - right.retentionOrder);
19
+ for (const unit of required) {
20
+ if (unit.events.length > remaining)
21
+ continue;
22
+ selected.set(unit.id, unit);
23
+ remaining -= unit.events.length;
24
+ }
25
+ for (const priority of PRIORITY_BANDS) {
26
+ if (remaining <= 0)
27
+ break;
28
+ const candidates = units.filter((unit) => (!selected.has(unit.id) && unit.priority === priority));
29
+ if (candidates.length === 0)
30
+ continue;
31
+ const totalSize = candidates.reduce((sum, unit) => sum + unit.events.length, 0);
32
+ if (totalSize <= remaining) {
33
+ for (const unit of candidates)
34
+ selected.set(unit.id, unit);
35
+ remaining -= totalSize;
36
+ continue;
37
+ }
38
+ remaining = selectSpreadCandidates(candidates, selected, units, remaining);
39
+ }
40
+ return [...selected.values()]
41
+ .flatMap((unit) => unit.events)
42
+ .sort((left, right) => left.order - right.order);
43
+ }
44
+ function semanticUnits(events, options) {
45
+ const resultByCall = new Map();
46
+ for (const event of events) {
47
+ if (event.kind !== 'tool_result')
48
+ continue;
49
+ const key = toolCorrelationKey(event);
50
+ resultByCall.set(key, [...(resultByCall.get(key) ?? []), event]);
51
+ }
52
+ const firstUserId = events.find((event) => event.kind === 'user_message')?.id;
53
+ const finalAssistantId = [...events].reverse()
54
+ .find((event) => event.kind === 'assistant_message')?.id;
55
+ const consumedResults = new Set();
56
+ const units = [];
57
+ for (const event of events) {
58
+ if (event.kind === 'tool_result' && consumedResults.has(event.id))
59
+ continue;
60
+ if (event.kind === 'tool_use') {
61
+ const result = resultByCall.get(toolCorrelationKey(event))
62
+ ?.find((candidate) => !consumedResults.has(candidate.id));
63
+ if (result)
64
+ consumedResults.add(result.id);
65
+ const exchange = result ? [event, result] : [event];
66
+ const failed = exchange.some((candidate) => candidate.isError || candidate.toolStatus === 'failure');
67
+ const pending = !result && options.preservePendingToolCalls === true;
68
+ units.push({
69
+ id: `tool:${event.id}`,
70
+ events: exchange,
71
+ order: event.order,
72
+ priority: failed ? 135 : pending ? 140 : 70,
73
+ required: failed || pending,
74
+ retentionOrder: pending ? -event.order : event.order,
75
+ });
76
+ continue;
77
+ }
78
+ const isFirstUser = event.id === firstUserId;
79
+ const isFinalAssistant = event.id === finalAssistantId;
80
+ const isBoundary = event.kind === 'lifecycle' && isBoundaryLifecycle(event);
81
+ const isFailedResult = event.kind === 'tool_result'
82
+ && (event.isError || event.toolStatus === 'failure');
83
+ const required = isFirstUser
84
+ || event.id === finalAssistantId
85
+ || isFailedResult
86
+ || isBoundary;
87
+ units.push({
88
+ id: `event:${event.id}`,
89
+ events: [event],
90
+ order: event.order,
91
+ priority: requiredPriority(event, {
92
+ isFirstUser,
93
+ isFinalAssistant,
94
+ isBoundary,
95
+ isFailedResult,
96
+ }),
97
+ required,
98
+ retentionOrder: event.order,
99
+ });
100
+ }
101
+ return units;
102
+ }
103
+ function requiredPriority(event, flags) {
104
+ if (flags.isFirstUser)
105
+ return 160;
106
+ if (flags.isFinalAssistant)
107
+ return 150;
108
+ if (flags.isBoundary && event.label === 'turn_started')
109
+ return 145;
110
+ if (flags.isFailedResult)
111
+ return 135;
112
+ if (flags.isBoundary)
113
+ return 130;
114
+ return semanticPriority(event);
115
+ }
116
+ function semanticPriority(event) {
117
+ if (event.kind === 'user_message' || event.kind === 'assistant_message')
118
+ return 90;
119
+ if (event.kind === 'skill_context')
120
+ return 80;
121
+ if (event.kind === 'runtime_context')
122
+ return event.runtimeKind === 'usage' ? 0 : 80;
123
+ if (event.kind === 'lifecycle')
124
+ return 70;
125
+ if (event.kind === 'tool_result')
126
+ return event.isError || event.toolStatus === 'failure' ? 100 : 70;
127
+ if (event.kind === 'observation' || event.kind === 'agent_activity')
128
+ return 60;
129
+ if (event.kind === 'model_activity')
130
+ return event.contentVisibility === 'plaintext' ? 40 : 20;
131
+ return 0;
132
+ }
133
+ function isBoundaryLifecycle(event) {
134
+ const label = `${event.label ?? ''} ${event.sourceType ?? ''}`.toLowerCase();
135
+ return /(?:turn|task)[_-]?(?:start|complete|abort|interrupt|end)/.test(label);
136
+ }
137
+ function selectSpreadCandidates(candidates, selected, allUnits, capacity) {
138
+ const selectedOrders = [...selected.values()].map((unit) => unit.order);
139
+ const firstOrder = allUnits[0]?.order ?? 0;
140
+ const lastOrder = allUnits.at(-1)?.order ?? firstOrder;
141
+ const distances = new Map(candidates.map((candidate) => [
142
+ candidate.id,
143
+ selectedOrders.length > 0
144
+ ? Math.min(...selectedOrders.map((order) => Math.abs(candidate.order - order)))
145
+ : Math.min(Math.abs(candidate.order - firstOrder), Math.abs(lastOrder - candidate.order)),
146
+ ]));
147
+ let remaining = capacity;
148
+ while (remaining > 0) {
149
+ const fitting = candidates.filter((unit) => (!selected.has(unit.id) && unit.events.length <= remaining));
150
+ if (fitting.length === 0)
151
+ break;
152
+ const next = fitting.reduce((best, candidate) => {
153
+ const candidateDistance = distances.get(candidate.id) ?? 0;
154
+ const bestDistance = distances.get(best.id) ?? 0;
155
+ return candidateDistance > bestDistance
156
+ || (candidateDistance === bestDistance && candidate.order < best.order)
157
+ ? candidate
158
+ : best;
159
+ });
160
+ selected.set(next.id, next);
161
+ remaining -= next.events.length;
162
+ for (const candidate of candidates) {
163
+ if (selected.has(candidate.id))
164
+ continue;
165
+ distances.set(candidate.id, Math.min(distances.get(candidate.id) ?? Number.POSITIVE_INFINITY, Math.abs(candidate.order - next.order)));
166
+ }
167
+ }
168
+ return remaining;
169
+ }
170
+ function toolCorrelationKey(event) {
171
+ return event.callInstanceId ?? event.toolUseId ?? event.id;
172
+ }
@@ -3,6 +3,7 @@ export declare const TASK_SEMANTIC_EVENT_LIMIT = 240;
3
3
  export interface ResolvedTaskWindow {
4
4
  events: ExperienceTimelineEvent[];
5
5
  semanticEvents: ExperienceTimelineEvent[];
6
+ relatedEvents: ExperienceTimelineEvent[];
6
7
  scope: TaskWindowScope;
7
8
  }
8
9
  export declare function resolveTaskWindow(session: TaskTrajectorySession, targetTurnId: string, semanticLimit?: number): ResolvedTaskWindow;
@@ -1,3 +1,5 @@
1
+ import { hasExplicitFollowUpCorrectionSignal } from './feedback-matchers.js';
2
+ import { projectTaskSemanticEvents } from './task-semantic-projection.js';
1
3
  export const TASK_SEMANTIC_EVENT_LIMIT = 240;
2
4
  export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SEMANTIC_EVENT_LIMIT) {
3
5
  const events = session.fullSessionTimeline;
@@ -11,11 +13,17 @@ export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SE
11
13
  return event ? [event] : [];
12
14
  })
13
15
  : [];
14
- const semanticEvents = semanticPreview(taskEvents, semanticLimit);
16
+ const semanticEvents = projectTaskSemanticEvents(taskEvents, semanticLimit, {
17
+ preservePendingToolCalls: selected?.status === 'open',
18
+ });
19
+ const relatedEvents = selected
20
+ ? relatedCorrectionEvents(session, selected.turnId, eventById)
21
+ : [];
15
22
  const matchedAttributedEventCount = taskEvents.filter((event) => attributedIds.has(event.id)).length;
16
23
  return {
17
24
  events: taskEvents,
18
25
  semanticEvents,
26
+ relatedEvents,
19
27
  scope: {
20
28
  basis: selected?.boundaryBasis ?? 'unresolved',
21
29
  turnId: selected?.turnId,
@@ -27,10 +35,23 @@ export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SE
27
35
  },
28
36
  };
29
37
  }
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)];
38
+ function relatedCorrectionEvents(session, selectedTurnId, eventById) {
39
+ const selectedIndex = session.turns.findIndex((turn) => turn.turnId === selectedTurnId);
40
+ const selectedTurn = selectedIndex >= 0 ? session.turns[selectedIndex] : undefined;
41
+ if (!selectedTurn)
42
+ return [];
43
+ const selectedTraceKey = turnTraceKey(selectedTurn);
44
+ const nextTurn = session.turns.slice(selectedIndex + 1).find((turn) => (turnTraceKey(turn) === selectedTraceKey));
45
+ if (!nextTurn)
46
+ return [];
47
+ const firstHumanMessage = nextTurn.eventIds
48
+ .map((id) => eventById.get(id))
49
+ .find((event) => event?.kind === 'user_message' && event.role === 'user');
50
+ if (!firstHumanMessage)
51
+ return [];
52
+ const text = firstHumanMessage.fullText ?? firstHumanMessage.snippet ?? '';
53
+ return hasExplicitFollowUpCorrectionSignal(text) ? [firstHumanMessage] : [];
54
+ }
55
+ function turnTraceKey(turn) {
56
+ return turn.traceId?.trim() || turn.sourceTrace;
36
57
  }
@@ -1,6 +1,7 @@
1
1
  import { inlineMarkdownText, renderSafeInlineMarkdown } from './inline-markdown.js';
2
2
  import { icon } from './icons.js';
3
3
  import { DEFAULT_LANG, e, layout } from './layout.js';
4
+ import { primaryTrajectoryEvidenceRef, trajectoryEvidenceRef, } from './trajectory-evidence.js';
4
5
  import { renderTrajectoryLiveClientSource } from './trajectory-live.js';
5
6
  import { renderTrajectoryRoutingClientSource } from './trajectory-routing.js';
6
7
  const ACCESS_LABELS = {
@@ -97,8 +98,8 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
97
98
  .app-main{width:100%;height:100%;min-height:0;max-width:none;margin:0;padding:10px 18px 14px;overflow:hidden}
98
99
  .footer{display:none!important}
99
100
  .trajectory-shell{height:100%;min-height:0;display:flex;flex-direction:column;margin:0;padding:0;letter-spacing:0}
100
- .trajectory-mode{display:inline-flex;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--bg-elevated)}
101
- .trajectory-mode button{height:28px;padding:3px 11px;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);font:500 12px/1.4 inherit;letter-spacing:0;cursor:pointer}
101
+ .trajectory-mode{display:inline-flex;flex:none;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--bg-elevated)}
102
+ .trajectory-mode button{height:28px;padding:3px 11px;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);font:500 12px/1.4 inherit;letter-spacing:0;white-space:nowrap;cursor:pointer}
102
103
  .trajectory-mode button[aria-pressed="true"]{background:var(--text-primary);color:var(--bg-surface);box-shadow:0 1px 2px rgba(24,32,51,.14)}
103
104
  .trajectory-mode button:focus-visible,.trajectory-event:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
104
105
  .trajectory-heading{flex:none;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center;min-height:32px;margin:0 0 8px}
@@ -113,7 +114,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
113
114
  .trajectory-warning strong{color:var(--text-primary)}
114
115
  .trajectory-frame{flex:1;min-height:0;overflow:hidden;display:grid;grid-template-rows:40px minmax(0,1fr);border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);box-shadow:var(--shadow-sm)}
115
116
  .trajectory-frame-head{display:flex;align-items:center;min-height:0;padding:0 10px 0 14px;border-bottom:1px solid var(--border)}
116
- .trajectory-frame-head h2{margin:0;font-size:13px;font-weight:600;letter-spacing:0}
117
+ .trajectory-frame-head h2{flex:none;margin:0;font-size:13px;font-weight:600;letter-spacing:0;white-space:nowrap}
117
118
  .trajectory-live-controls{display:inline-flex;align-items:center;gap:3px;margin-left:8px}
118
119
  .trajectory-live-state{display:inline-flex;align-items:center;gap:5px;color:var(--text-muted);font-size:9px;white-space:nowrap}
119
120
  .trajectory-live-state:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px rgba(31,157,99,.1)}
@@ -249,6 +250,11 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
249
250
  .trajectory-operation-copy p{display:-webkit-box;margin:0;overflow:hidden;color:var(--text-secondary);font-size:11px;line-height:1.45;-webkit-box-orient:vertical;-webkit-line-clamp:2}
250
251
  .trajectory-operation-actions{display:flex;align-items:center;gap:6px;padding-top:0}
251
252
  .trajectory-evidence-count{color:var(--text-secondary);font-size:10px;white-space:nowrap}
253
+ .trajectory-evidence-jump,.trajectory-field-evidence{border:0;border-radius:4px;background:transparent;color:var(--accent);font-size:9px;font-weight:600;white-space:nowrap;cursor:pointer}
254
+ .trajectory-evidence-jump{padding:4px 6px}
255
+ .trajectory-field-evidence{padding:3px 5px}
256
+ .trajectory-evidence-jump:hover,.trajectory-field-evidence:hover{background:var(--bg-elevated);color:var(--text-primary)}
257
+ .trajectory-evidence-jump:focus-visible,.trajectory-field-evidence:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
252
258
  .trajectory-inspector-close{display:grid;width:24px;height:24px;place-items:center;border:0;border-radius:4px;background:transparent;color:var(--text-muted);cursor:pointer}
253
259
  .trajectory-inspector-close:hover{background:var(--bg-elevated);color:var(--text-primary)}
254
260
  .trajectory-inspector-close:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
@@ -284,6 +290,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
284
290
  .trajectory-raw-head,.trajectory-raw-row summary{display:grid;grid-template-columns:76px 126px minmax(240px,1fr) 138px;gap:12px;align-items:center}
285
291
  .trajectory-raw-head{position:sticky;top:0;z-index:3;min-height:32px;padding:0 14px;border-bottom:1px solid var(--border);background:var(--bg-elevated);color:var(--text-muted);font-size:9px;font-weight:600}
286
292
  .trajectory-raw-row{border-bottom:1px solid var(--border)}
293
+ .trajectory-raw-row.is-evidence-target{background:rgba(79,70,229,.07);box-shadow:inset 3px 0 0 var(--accent)}
287
294
  .trajectory-raw-row[hidden]{display:none}
288
295
  .trajectory-raw-row summary{min-height:46px;padding:7px 14px;color:var(--text-primary);cursor:pointer;list-style:none}
289
296
  .trajectory-raw-row summary::-webkit-details-marker{display:none}
@@ -301,7 +308,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
301
308
  @media(max-width:1100px){.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:minmax(0,1fr) 320px}.trajectory-canvas{--lane-label-width:96px;--event-width:148px}.trajectory-lane-name{padding-inline:10px}.trajectory-lane-name span{font-size:9px}}
302
309
  @media(max-width:1080px){.trajectory-meta-time{display:none!important}}
303
310
  @media(max-width:860px){.app-main{padding-inline:10px}.trajectory-heading{grid-template-columns:1fr;gap:0}.trajectory-meta{display:none}.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:1fr;grid-template-rows:minmax(0,3fr) minmax(160px,2fr)}.trajectory-inspector{border-top:1px solid var(--border);border-left:0}.trajectory-operation-head{padding-block:9px}}
304
- @media(max-width:600px){.app-bar{padding-inline:10px}.app-brand-tag{display:none}.trajectory-heading h1{font-size:17px}.trajectory-range{display:none}.trajectory-frame-head .trajectory-mode{margin-left:auto}.trajectory-canvas{--lane-label-width:76px;--event-width:126px}.trajectory-lane-name{padding-inline:8px}.trajectory-lane-name span{display:none}.trajectory-event-time{display:none}.trajectory-raw-head,.trajectory-raw-row summary{grid-template-columns:66px 94px minmax(160px,1fr)}.trajectory-raw-id{display:none}.trajectory-raw-row pre{padding-left:14px}}
311
+ @media(max-width:600px){.app-bar{padding-inline:10px}.app-brand-tag{display:none}.trajectory-heading h1{font-size:17px}.trajectory-frame-head{gap:2px;padding-inline:8px}.trajectory-frame-space{min-width:0}.trajectory-range{display:none}.trajectory-live-controls{margin-left:3px}.trajectory-live-state{gap:0;font-size:0}.trajectory-live-follow{width:22px;padding:0;justify-content:center}.trajectory-live-follow span{display:none}.trajectory-boundary-info{margin-left:1px}.trajectory-focus{margin-left:2px}.trajectory-frame-head .trajectory-mode{margin-left:2px}.trajectory-mode button{min-width:37px;padding-inline:6px;font-size:0}.trajectory-mode button:after{content:attr(data-short-label);font-size:10px}.trajectory-canvas{--lane-label-width:76px;--event-width:126px}.trajectory-lane-name{padding-inline:8px}.trajectory-lane-name span{display:none}.trajectory-event-time{display:none}.trajectory-raw-head,.trajectory-raw-row summary{grid-template-columns:66px 94px minmax(160px,1fr)}.trajectory-raw-id{display:none}.trajectory-raw-row pre{padding-left:14px}}
305
312
  @media(prefers-reduced-motion:reduce){.trajectory-body{transition:none}}
306
313
  </style>
307
314
  <main class="trajectory-shell" data-mode="semantic"${options.live ? ` data-live-endpoint="${e(options.live.endpoint)}" data-live-revision="${e(options.live.revision)}"` : ''}>
@@ -333,9 +340,9 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
333
340
  <span class="trajectory-range">${e(formatRelativeTime(0))} — ${e(formatRelativeTime(projection.durationMs))}</span>
334
341
  ${projection.facets.length > 0 ? renderFacetFocus(projection.facets, lang) : ''}
335
342
  <div class="trajectory-mode" aria-label="${zh ? '查看模式' : 'View mode'}">
336
- <button type="button" data-trajectory-mode="semantic" aria-pressed="true">${zh ? '语义轨迹' : 'Semantic trajectory'}</button>
337
- <button type="button" data-trajectory-mode="normalized" aria-pressed="false">${zh ? '规范化事件' : 'Normalized events'}</button>
338
- <button type="button" data-trajectory-mode="source" aria-pressed="false">${zh ? '原始日志' : 'Raw logs'}</button>
343
+ <button type="button" data-trajectory-mode="semantic" data-short-label="${zh ? '轨迹' : 'Track'}" aria-pressed="true">${zh ? '语义轨迹' : 'Semantic trajectory'}</button>
344
+ <button type="button" data-trajectory-mode="normalized" data-short-label="${zh ? '事件' : 'Events'}" aria-pressed="false">${zh ? '规范化事件' : 'Normalized events'}</button>
345
+ <button type="button" data-trajectory-mode="source" data-short-label="${zh ? '日志' : 'Logs'}" aria-pressed="false">${zh ? '原始日志' : 'Raw logs'}</button>
339
346
  </div>
340
347
  </header>
341
348
 
@@ -358,7 +365,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
358
365
  <header class="trajectory-operation-head">
359
366
  <div class="trajectory-operation-type" id="trajectory-operation-type"></div>
360
367
  <div class="trajectory-operation-copy"><h3 id="trajectory-operation-title"></h3><p id="trajectory-operation-summary"></p></div>
361
- <div class="trajectory-operation-actions"><div class="trajectory-evidence-count" id="trajectory-evidence-count"></div><button class="trajectory-inspector-close" type="button" data-inspector-close aria-label="${zh ? '关闭详情' : 'Close details'}" title="${zh ? '关闭详情' : 'Close details'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6L6 18"></path></svg></button></div>
368
+ <div class="trajectory-operation-actions"><div class="trajectory-evidence-count" id="trajectory-evidence-count"></div><button class="trajectory-evidence-jump" type="button" data-operation-evidence hidden></button><button class="trajectory-inspector-close" type="button" data-inspector-close aria-label="${zh ? '关闭详情' : 'Close details'}" title="${zh ? '关闭详情' : 'Close details'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6L6 18"></path></svg></button></div>
362
369
  </header>
363
370
  <div class="trajectory-semantic-panels">${projection.operations.map((operation) => renderSemanticPanel(operation, false, lang)).join('')}</div>
364
371
  </aside>
@@ -377,7 +384,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
377
384
  const cards = Array.from(document.querySelectorAll('[data-trajectory-operation]'));
378
385
  const normalizedRows = Array.from(document.querySelectorAll('[data-trajectory-normalized-event]'));
379
386
  const normalizedEmpty = document.querySelector('[data-trajectory-normalized-empty]');
380
- const sourceRecordList = document.querySelector('[data-source-records-endpoint]');
387
+ const sourceRecordList = document.querySelector('[data-event-view="source"]');
381
388
  const semanticPanels = Array.from(document.querySelectorAll('[data-trajectory-semantic-panel]'));
382
389
  const semanticPanelContainer = document.querySelector('.trajectory-semantic-panels');
383
390
  const detailBlocks = Array.from(document.querySelectorAll('[data-field-detail-block]'));
@@ -385,6 +392,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
385
392
  const operationTitle = document.querySelector('#trajectory-operation-title');
386
393
  const operationSummary = document.querySelector('#trajectory-operation-summary');
387
394
  const evidenceCount = document.querySelector('#trajectory-evidence-count');
395
+ const operationEvidence = document.querySelector('[data-operation-evidence]');
388
396
  const lanes = document.querySelector('.trajectory-lanes');
389
397
  const operationLinks = document.querySelector('.trajectory-links');
390
398
  const facetFocus = document.querySelector('.trajectory-focus');
@@ -410,6 +418,8 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
410
418
  loadFailed: zh ? '原始日志读取失败。可以切换视图后重试。' : 'Raw logs could not be loaded. Switch views and retry.',
411
419
  partialRecords: SOURCE_RECORD_PARTIAL_LABELS[lang].records,
412
420
  partialContent: SOURCE_RECORD_PARTIAL_LABELS[lang].content,
421
+ viewSource: zh ? '查看原始日志' : 'View raw log',
422
+ viewNormalized: zh ? '查看规范化事件' : 'View normalized event',
413
423
  })};
414
424
  const cardLane = (card) => card.closest('.trajectory-lane')?.dataset.lane || '';
415
425
  const operationIds = semanticPanels.map((panel) => panel.dataset.trajectorySemanticPanel).filter(Boolean);
@@ -815,6 +825,16 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
815
825
  if (operationTitle) operationTitle.textContent = panel.dataset.title || '—';
816
826
  if (operationSummary) operationSummary.textContent = panel.dataset.summary || '';
817
827
  if (evidenceCount) evidenceCount.textContent = panel.dataset.evidenceLabel || '';
828
+ if (operationEvidence) {
829
+ const sourceEventId = panel.dataset.evidenceSourceEventId || '';
830
+ operationEvidence.hidden = !sourceEventId;
831
+ operationEvidence.dataset.evidenceSourceEventId = sourceEventId;
832
+ operationEvidence.dataset.evidenceSourceLineIndex = panel.dataset.evidenceSourceLineIndex || '';
833
+ operationEvidence.dataset.evidenceTraceId = panel.dataset.evidenceTraceId || '';
834
+ operationEvidence.textContent = panel.dataset.evidenceSourceLineIndex
835
+ ? sourceRecordLabels.viewSource
836
+ : sourceRecordLabels.viewNormalized;
837
+ }
818
838
  currentOperationId = id;
819
839
  if (animateLayout && shell.dataset.inspectorOpen !== 'true') beginLayoutTransition();
820
840
  shell.dataset.inspectorOpen = 'true';
@@ -831,6 +851,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
831
851
  });
832
852
  semanticPanels.forEach((item) => { item.hidden = true; });
833
853
  currentOperationId = '';
854
+ if (operationEvidence) operationEvidence.hidden = true;
834
855
  if (inspectorWasOpen) beginLayoutTransition();
835
856
  delete shell.dataset.inspectorOpen;
836
857
  if (inspector) inspector.hidden = true;
@@ -896,6 +917,8 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
896
917
  if (!sourceRecordList) return;
897
918
  const details = document.createElement('details');
898
919
  details.className = 'trajectory-raw-row';
920
+ details.dataset.sourceLineIndex = String(record.sourceIndex ?? '');
921
+ details.dataset.sourceTraceId = String(record.traceId || '');
899
922
  const summary = document.createElement('summary');
900
923
  const time = document.createElement('time');
901
924
  time.textContent = relativeSourceTime(record.timestamp);
@@ -955,6 +978,40 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
955
978
  if (mode === 'source') void loadSourceRecords();
956
979
  document.querySelectorAll('[data-trajectory-mode]').forEach((item) => item.setAttribute('aria-pressed', String(item.dataset.trajectoryMode === mode)));
957
980
  };
981
+ const clearEvidenceTarget = () => {
982
+ document.querySelectorAll('.trajectory-raw-row.is-evidence-target').forEach((row) => row.classList.remove('is-evidence-target'));
983
+ };
984
+ const revealEvidenceRow = (row) => {
985
+ if (!row) return false;
986
+ clearEvidenceTarget();
987
+ row.open = true;
988
+ row.classList.add('is-evidence-target');
989
+ requestAnimationFrame(() => row.scrollIntoView({
990
+ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
991
+ block: 'center',
992
+ }));
993
+ return true;
994
+ };
995
+ const matchingSourceRow = (lineIndex, traceId) => Array.from(sourceRecordList?.querySelectorAll('[data-source-line-index]') || [])
996
+ .find((row) => row.dataset.sourceLineIndex === lineIndex
997
+ && (!traceId || row.dataset.sourceTraceId === traceId));
998
+ const revealEvidence = async (target) => {
999
+ const sourceEventId = target.dataset.evidenceSourceEventId || target.dataset.detailSourceEventId || '';
1000
+ const sourceLineIndex = target.dataset.evidenceSourceLineIndex || target.dataset.detailSourceLineIndex || '';
1001
+ const traceId = target.dataset.evidenceTraceId || target.dataset.detailTraceId || '';
1002
+ if (sourceLineIndex && sourceRecordList) {
1003
+ setTrajectoryMode('source');
1004
+ await loadSourceRecords();
1005
+ if (revealEvidenceRow(matchingSourceRow(sourceLineIndex, traceId))) return;
1006
+ }
1007
+ if (!sourceEventId) return;
1008
+ setTrajectoryMode('normalized');
1009
+ revealEvidenceRow(normalizedRows.find((row) => row.dataset.trajectoryNormalizedEvent === sourceEventId));
1010
+ };
1011
+ operationEvidence?.addEventListener('click', () => void revealEvidence(operationEvidence), { signal: pageLifecycle.signal });
1012
+ document.querySelectorAll('[data-view-evidence]').forEach((button) => {
1013
+ button.addEventListener('click', () => void revealEvidence(button.closest('[data-field-detail-block]') || button), { signal: pageLifecycle.signal });
1014
+ });
958
1015
  document.querySelectorAll('[data-trajectory-mode]').forEach((button) => {
959
1016
  button.addEventListener('click', () => {
960
1017
  const mode = button.dataset.trajectoryMode || 'semantic';
@@ -1208,8 +1265,8 @@ function projectReplay(model, lang, options) {
1208
1265
  : (zh ? '已观测到工具调用,但当前 trace 中没有匹配到返回结果。' : 'A tool call was observed, but no matching result appears in the trace.'),
1209
1266
  evidenceLabel: zh ? `${step.events.length + evidence.length} 条关联证据` : `${step.events.length + evidence.length} related records`,
1210
1267
  fields: [
1211
- { label: zh ? '执行' : 'Action', value: `${call?.toolName ?? step.title} · ${inferToolActionLabel(evidence, lang)}`, detail: compactText(input || (zh ? '未记录输入' : 'Input not recorded'), 520), detailKind: 'content' },
1212
- { label: zh ? '结果' : 'Result', value: `${toolStatusLabel(resultState, lang)}${duration ? ` · ${duration}` : ''}`, detail: result ? eventPreview(result, zh ? '工具没有返回内容。' : 'The tool returned no content.') : resultState === 'pending' ? (zh ? '正在等待工具结果写入 trace。' : 'Waiting for the tool result to appear in the trace.') : (zh ? '当前 trace 中没有匹配到工具结果。' : 'No matching tool result in this trace.'), detailSourceEventId: result?.id, detailKind: 'result' },
1268
+ { label: zh ? '执行' : 'Action', value: `${call?.toolName ?? step.title} · ${inferToolActionLabel(evidence, lang)}`, detail: compactText(input || (zh ? '未记录输入' : 'Input not recorded'), 520), evidence: trajectoryEvidenceRef(call), detailKind: 'content' },
1269
+ { label: zh ? '结果' : 'Result', value: `${toolStatusLabel(resultState, lang)}${duration ? ` · ${duration}` : ''}`, detail: result ? eventPreview(result, zh ? '工具没有返回内容。' : 'The tool returned no content.') : resultState === 'pending' ? (zh ? '正在等待工具结果写入 trace。' : 'Waiting for the tool result to appear in the trace.') : (zh ? '当前 trace 中没有匹配到工具结果。' : 'No matching tool result in this trace.'), evidence: trajectoryEvidenceRef(result), detailKind: 'result' },
1213
1270
  { label: 'Knowledge', value: evidence.length > 0 ? evidence.map((item) => `${knowledgeKindLabel(item, lang)} · ${item.label}`).join('、') : (zh ? '未关联' : 'Not associated'), detail: evidence.length > 0 ? evidence.map((item) => `${item.accessKind} · ${shortHash(item.contentHash)}`).join('\n') : (zh ? '未从本次工具交换投影出 Knowledge' : 'No Knowledge projected from this tool exchange') },
1214
1271
  ],
1215
1272
  events: step.events,
@@ -1256,7 +1313,7 @@ function projectReplay(model, lang, options) {
1256
1313
  value: contextContent ? (zh ? '源日志已记录可见内容' : 'Observable content recorded') : (zh ? '源日志未记录上下文内容' : 'Context content not recorded by the source log'),
1257
1314
  detail: contextContent ?? (zh ? '当前规范化事件只保留了上下文类型与元数据。' : 'The normalized event only retains the context type and metadata.'),
1258
1315
  copyable: Boolean(contextContent),
1259
- detailSourceEventId: contextContent ? contextEvent?.id : undefined,
1316
+ evidence: trajectoryEvidenceRef(contextEvent),
1260
1317
  detailKind: 'content',
1261
1318
  },
1262
1319
  { label: zh ? '内容身份' : 'Content identity', value: first?.contentHash ? `sha256:${shortHash(first.contentHash)}` : (zh ? '未记录哈希' : 'Hash not recorded'), detail: zh ? '只标识观测到的内容,不推断是否被模型采用' : 'Identifies observed content without inferring model use' },
@@ -1329,6 +1386,7 @@ function projectReplay(model, lang, options) {
1329
1386
  value: text,
1330
1387
  detail: reasoningContentSourceLabel(event?.contentSource, lang),
1331
1388
  presentation: 'content',
1389
+ evidence: trajectoryEvidenceRef(event),
1332
1390
  }]),
1333
1391
  ]
1334
1392
  : [
@@ -1340,6 +1398,7 @@ function projectReplay(model, lang, options) {
1340
1398
  value: text,
1341
1399
  detail: zh ? '未做隐藏意图或原因推断' : 'No hidden-intent or causal inference',
1342
1400
  presentation: 'content',
1401
+ evidence: trajectoryEvidenceRef(event),
1343
1402
  },
1344
1403
  ],
1345
1404
  events: step.events,
@@ -1602,7 +1661,8 @@ function renderFacetFocus(facets, lang) {
1602
1661
  return `<details class="trajectory-focus"><summary aria-label="${zh ? '类型筛选' : 'Filter by type'}" title="${zh ? '类型筛选' : 'Filter by type'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"></path><path d="M7 12h10"></path><path d="M10 18h4"></path></svg></summary><div class="trajectory-focus-menu" role="group" aria-label="${zh ? '类型筛选' : 'Type filter'}"><span class="trajectory-focus-title">${zh ? '类型筛选' : 'Type filter'}</span><button class="trajectory-focus-option" type="button" data-trajectory-facet="" aria-pressed="true"><span class="trajectory-focus-swatch" aria-hidden="true"></span><span>${zh ? '全部类型' : 'All types'}</span><small>${zh ? '清除' : 'Clear'}</small></button>${options}</div></details>`;
1603
1662
  }
1604
1663
  function renderSemanticPanel(operation, selected, lang) {
1605
- return `<div class="trajectory-operation-panel trajectory-fields" data-trajectory-semantic-panel="${e(operation.id)}" data-selection-label="${e(operation.selectionLabel)}" data-type-label="${e(operation.typeLabel)}" data-title="${e(operation.title)}" data-summary="${e(operation.summary)}" data-evidence-label="${e(operation.evidenceLabel)}"${selected ? '' : ' hidden'}>${operation.fields.map((field) => {
1664
+ const evidence = primaryTrajectoryEvidenceRef(operation.events);
1665
+ return `<div class="trajectory-operation-panel trajectory-fields" data-trajectory-semantic-panel="${e(operation.id)}" data-selection-label="${e(operation.selectionLabel)}" data-type-label="${e(operation.typeLabel)}" data-title="${e(operation.title)}" data-summary="${e(operation.summary)}" data-evidence-label="${e(operation.evidenceLabel)}"${renderEvidenceDataAttributes(evidence)}${selected ? '' : ' hidden'}>${operation.fields.map((field) => {
1606
1666
  const content = field.presentation === 'content'
1607
1667
  ? `<div class="trajectory-field-content">${renderSafeInlineMarkdown(field.value)}</div>`
1608
1668
  : `<strong class="trajectory-field-value">${e(field.value)}</strong>`;
@@ -1611,7 +1671,7 @@ function renderSemanticPanel(operation, selected, lang) {
1611
1671
  }
1612
1672
  function renderFieldDetail(field, lang) {
1613
1673
  const preview = fieldDetailPreview(field.detail);
1614
- if ((!preview.truncated || !field.detailSourceEventId) && !field.copyable) {
1674
+ if ((!preview.truncated || !field.evidence?.normalizedEventId) && !field.copyable && !field.evidence) {
1615
1675
  return `<code class="trajectory-field-detail">${e(field.detail)}</code>`;
1616
1676
  }
1617
1677
  const zh = lang === 'zh';
@@ -1624,18 +1684,30 @@ function renderFieldDetail(field, lang) {
1624
1684
  const collapseLabel = zh ? (result ? '收起结果' : '收起内容') : (result ? 'Collapse result' : 'Collapse content');
1625
1685
  const copyLabel = zh ? (result ? '复制结果' : '复制内容') : (result ? 'Copy result' : 'Copy content');
1626
1686
  const copiedLabel = zh ? '已复制' : 'Copied';
1627
- const canExpand = preview.truncated && Boolean(field.detailSourceEventId);
1687
+ const canExpand = preview.truncated && Boolean(field.evidence?.normalizedEventId);
1628
1688
  const displayedDetail = canExpand ? preview.text : field.detail;
1629
- const sourceEventAttribute = field.detailSourceEventId
1630
- ? ` data-detail-source-event-id="${e(field.detailSourceEventId)}"`
1631
- : '';
1689
+ const evidenceAttributes = renderEvidenceDataAttributes(field.evidence, 'detail');
1632
1690
  const status = canExpand
1633
1691
  ? `<span class="trajectory-field-detail-status" data-field-detail-status>${e(previewStatus)}</span>`
1634
1692
  : '<span></span>';
1635
1693
  const toggle = canExpand
1636
1694
  ? `<button class="trajectory-field-detail-toggle" type="button" data-field-detail-toggle aria-expanded="false">${e(expandLabel)}</button>`
1637
1695
  : '';
1638
- return `<div class="trajectory-field-detail-block" data-field-detail-block data-expanded="false" data-expandable="${String(canExpand)}"${sourceEventAttribute} data-preview-status="${e(previewStatus)}" data-full-status="${e(fullStatus)}" data-expand-label="${e(expandLabel)}" data-collapse-label="${e(collapseLabel)}" data-copy-label="${e(copyLabel)}" data-copied-label="${e(copiedLabel)}"><code class="trajectory-field-detail" data-field-detail>${e(displayedDetail)}</code><div class="trajectory-field-detail-actions">${status}<span class="trajectory-field-detail-controls">${toggle}<button class="trajectory-field-detail-copy" type="button" data-field-detail-copy aria-label="${e(copyLabel)}" title="${e(copyLabel)}">${icon('copy', { size: 13 })}</button></span></div></div>`;
1696
+ const evidenceButton = field.evidence
1697
+ ? `<button class="trajectory-field-evidence" type="button" data-view-evidence>${lang === 'zh' ? (field.evidence.sourceLineIndex !== undefined ? '查看原始日志' : '查看规范化事件') : (field.evidence.sourceLineIndex !== undefined ? 'View raw log' : 'View normalized event')}</button>`
1698
+ : '';
1699
+ return `<div class="trajectory-field-detail-block" data-field-detail-block data-expanded="false" data-expandable="${String(canExpand)}"${evidenceAttributes} data-preview-status="${e(previewStatus)}" data-full-status="${e(fullStatus)}" data-expand-label="${e(expandLabel)}" data-collapse-label="${e(collapseLabel)}" data-copy-label="${e(copyLabel)}" data-copied-label="${e(copiedLabel)}"><code class="trajectory-field-detail" data-field-detail>${e(displayedDetail)}</code><div class="trajectory-field-detail-actions">${status}<span class="trajectory-field-detail-controls">${evidenceButton}${toggle}<button class="trajectory-field-detail-copy" type="button" data-field-detail-copy aria-label="${e(copyLabel)}" title="${e(copyLabel)}">${icon('copy', { size: 13 })}</button></span></div></div>`;
1700
+ }
1701
+ function renderEvidenceDataAttributes(evidence, prefix = 'evidence') {
1702
+ if (!evidence)
1703
+ return '';
1704
+ const attributes = [` data-${prefix}-source-event-id="${e(evidence.normalizedEventId)}"`];
1705
+ if (evidence.sourceLineIndex !== undefined) {
1706
+ attributes.push(` data-${prefix}-source-line-index="${evidence.sourceLineIndex}"`);
1707
+ }
1708
+ if (evidence.traceId)
1709
+ attributes.push(` data-${prefix}-trace-id="${e(evidence.traceId)}"`);
1710
+ return attributes.join('');
1639
1711
  }
1640
1712
  function fieldDetailPreview(detail) {
1641
1713
  const lines = detail.replace(/\r\n/g, '\n').split('\n');
@@ -1669,7 +1741,8 @@ function renderNormalizedEventList(events, operations, startTimestamp, lang) {
1669
1741
  const source = `${event.sourceLineIndex !== undefined ? `#${event.sourceLineIndex} · ` : ''}${shortHash(event.traceId ?? event.id)}`;
1670
1742
  const facetIds = [...(facetsByEventId.get(event.id) ?? [])];
1671
1743
  const kind = event.sourceType ? `${event.kind} · ${event.sourceType}` : event.kind;
1672
- return `<details class="trajectory-raw-row" data-trajectory-normalized-event="${e(event.id)}" data-trajectory-facets="${e(facetIds.join(' '))}"><summary><time>${e(formatRelativeTimestamp(event.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(kind)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(source)}</code></summary><pre>${e(content || event.label || '')}</pre></details>`;
1744
+ const sourceAttributes = `${event.sourceLineIndex !== undefined ? ` data-source-line-index="${event.sourceLineIndex}"` : ''}${event.traceId ? ` data-source-trace-id="${e(event.traceId)}"` : ''}`;
1745
+ return `<details class="trajectory-raw-row" data-trajectory-normalized-event="${e(event.id)}"${sourceAttributes} data-trajectory-facets="${e(facetIds.join(' '))}"><summary><time>${e(formatRelativeTimestamp(event.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(kind)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(source)}</code></summary><pre>${e(content || event.label || '')}</pre></details>`;
1673
1746
  }).join('');
1674
1747
  return `<section class="trajectory-raw-list" data-event-view="normalized" aria-label="${zh ? '按来源顺序排列的规范化事件' : 'Normalized events in source order'}"><header class="trajectory-raw-head"><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '规范化 / 来源类型' : 'Normalized / source type'}</span><span>${zh ? '内容' : 'Content'}</span><span>${zh ? '来源位置' : 'Source position'}</span></header>${rows}<div class="trajectory-raw-empty" data-trajectory-normalized-empty hidden>${zh ? '没有符合当前类型筛选的规范化事件' : 'No normalized events match the current type filter'}</div></section>`;
1675
1748
  }
@@ -1721,7 +1794,7 @@ function renderSourceRecordList(model, startTimestamp, lang, sourceRecordsEndpoi
1721
1794
  record.redacted ? (zh ? '【不透明加密载荷已省略】' : '[Opaque encrypted payload omitted]') : '',
1722
1795
  record.truncated ? (zh ? '【该记录已按归档上限截断】' : '[Record truncated by archive limit]') : '',
1723
1796
  ].filter(Boolean).join('\n');
1724
- return `<details class="trajectory-raw-row"><summary><time>${e(formatRelativeTimestamp(record.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(record.sourceType)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(locator)}</code></summary><pre>${e(record.raw)}${notices ? `\n\n${e(notices)}` : ''}</pre></details>`;
1797
+ return `<details class="trajectory-raw-row" data-source-line-index="${record.sourceIndex}" data-source-trace-id="${e(record.traceId)}"><summary><time>${e(formatRelativeTimestamp(record.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(record.sourceType)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(locator)}</code></summary><pre>${e(record.raw)}${notices ? `\n\n${e(notices)}` : ''}</pre></details>`;
1725
1798
  }).join('');
1726
1799
  const unavailable = source.status === 'unavailable'
1727
1800
  ? `<div class="trajectory-raw-empty">${e(sourceRecordUnavailableLabel(source.reason, lang))}</div>`
@@ -0,0 +1,8 @@
1
+ import type { ExperienceTimelineEvent } from '../types/index.js';
2
+ export interface TrajectoryEvidenceRef {
3
+ normalizedEventId: string;
4
+ sourceLineIndex?: number;
5
+ traceId?: string;
6
+ }
7
+ export declare function trajectoryEvidenceRef(event: ExperienceTimelineEvent | undefined): TrajectoryEvidenceRef | undefined;
8
+ export declare function primaryTrajectoryEvidenceRef(events: ExperienceTimelineEvent[]): TrajectoryEvidenceRef | undefined;
@@ -0,0 +1,12 @@
1
+ export function trajectoryEvidenceRef(event) {
2
+ if (!event)
3
+ return undefined;
4
+ return {
5
+ normalizedEventId: event.id,
6
+ ...(event.sourceLineIndex !== undefined ? { sourceLineIndex: event.sourceLineIndex } : {}),
7
+ ...(event.traceId ? { traceId: event.traceId } : {}),
8
+ };
9
+ }
10
+ export function primaryTrajectoryEvidenceRef(events) {
11
+ return trajectoryEvidenceRef(events.find((event) => event.sourceLineIndex !== undefined) ?? events[0]);
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-knowledge",
3
- "version": "0.52.1",
3
+ "version": "0.52.2",
4
4
  "packageManager": "yarn@4.16.0",
5
5
  "description": "OMK — Observe. Measure. Know. Evidence-backed knowledge changes for AI applications.",
6
6
  "type": "module",