oh-my-knowledge 0.47.0 → 0.49.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 (246) hide show
  1. package/README.md +50 -18
  2. package/README.zh.md +55 -23
  3. package/dist/analysis/coverage-analyzer.d.ts +1 -0
  4. package/dist/analysis/coverage-analyzer.js +125 -62
  5. package/dist/analysis/failure-clusterer.js +2 -1
  6. package/dist/analysis/gap-analyzer.d.ts +2 -2
  7. package/dist/analysis/gap-analyzer.js +13 -3
  8. package/dist/analysis/hedging-classifier.d.ts +2 -2
  9. package/dist/analysis/hedging-classifier.js +3 -4
  10. package/dist/analysis/report-diagnostics.js +9 -7
  11. package/dist/analysis/sample-diagnostics.js +6 -6
  12. package/dist/artifact-graph/doctor.js +15 -7
  13. package/dist/assets/agent-skills/omk/SKILL.md +27 -7
  14. package/dist/assets/agent-skills/omk/references/commands.md +19 -17
  15. package/dist/authoring/evolver.d.ts +10 -6
  16. package/dist/authoring/evolver.js +496 -83
  17. package/dist/authoring/generator.d.ts +3 -3
  18. package/dist/authoring/generator.js +5 -10
  19. package/dist/authoring/sample-fixer.d.ts +8 -6
  20. package/dist/authoring/sample-fixer.js +76 -5
  21. package/dist/cli/commands/doctor.js +31 -14
  22. package/dist/cli/commands/eval/index.d.ts +3 -0
  23. package/dist/cli/commands/eval/index.js +163 -17
  24. package/dist/cli/commands/evolve.d.ts +4 -4
  25. package/dist/cli/commands/evolve.js +27 -13
  26. package/dist/cli/commands/init.js +16 -3
  27. package/dist/cli/commands/observe/inbox.js +44 -21
  28. package/dist/cli/commands/observe/index.js +20 -11
  29. package/dist/cli/commands/observe/ingest.d.ts +3 -0
  30. package/dist/cli/commands/observe/ingest.js +35 -4
  31. package/dist/cli/commands/sample.d.ts +9 -3
  32. package/dist/cli/commands/sample.js +91 -74
  33. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  34. package/dist/cli/lib/codex-model-hint.d.ts +9 -0
  35. package/dist/cli/lib/codex-model-hint.js +45 -0
  36. package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
  37. package/dist/cli/lib/generation-failure-hint.js +61 -0
  38. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  39. package/dist/cli/lib/i18n-dict/common.js +4 -0
  40. package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
  41. package/dist/cli/lib/i18n-dict/gen.js +38 -6
  42. package/dist/cli/lib/i18n-dict/help.js +6 -6
  43. package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
  44. package/dist/cli/lib/i18n-dict/init.js +13 -9
  45. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  46. package/dist/cli/lib/i18n-dict/run.js +34 -2
  47. package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
  48. package/dist/cli/lib/llm-failure-classifier.js +8 -0
  49. package/dist/cli/lib/parse-run-config.d.ts +6 -5
  50. package/dist/cli/lib/parse-run-config.js +16 -9
  51. package/dist/cli/lib/runtime-defaults.d.ts +21 -0
  52. package/dist/cli/lib/runtime-defaults.js +79 -0
  53. package/dist/diagnosis/observe-mapper.js +14 -15
  54. package/dist/diagnosis/observe-producer.js +3 -1
  55. package/dist/diagnosis/studio-projection.js +14 -7
  56. package/dist/diagnosis/types.d.ts +2 -0
  57. package/dist/diagnosis/types.js +12 -0
  58. package/dist/doctor/endpoint-rule.js +2 -1
  59. package/dist/eval-core/artifact-file-names.js +18 -1
  60. package/dist/eval-core/artifact-index.d.ts +7 -11
  61. package/dist/eval-core/artifact-index.js +139 -80
  62. package/dist/eval-core/cache.d.ts +12 -3
  63. package/dist/eval-core/cache.js +89 -29
  64. package/dist/eval-core/comparability.js +10 -6
  65. package/dist/eval-core/evaluation-execution.d.ts +2 -1
  66. package/dist/eval-core/evaluation-execution.js +122 -37
  67. package/dist/eval-core/evaluation-job.d.ts +4 -1
  68. package/dist/eval-core/evaluation-job.js +4 -1
  69. package/dist/eval-core/evaluation-reporting.d.ts +15 -13
  70. package/dist/eval-core/evaluation-reporting.js +54 -52
  71. package/dist/eval-core/execution-strategy.d.ts +2 -0
  72. package/dist/eval-core/execution-strategy.js +11 -9
  73. package/dist/eval-core/fact-checker.js +15 -7
  74. package/dist/eval-core/holdout.js +3 -2
  75. package/dist/eval-core/judge-independence.d.ts +2 -2
  76. package/dist/eval-core/mock-hook.cjs +23 -6
  77. package/dist/eval-core/mocks-runtime.js +30 -8
  78. package/dist/eval-core/report-document.d.ts +12 -0
  79. package/dist/eval-core/report-document.js +1151 -0
  80. package/dist/eval-core/report-extensions.d.ts +4 -0
  81. package/dist/eval-core/report-extensions.js +500 -0
  82. package/dist/eval-core/report-file-migration.js +7 -2
  83. package/dist/eval-core/resume-compatibility.d.ts +31 -0
  84. package/dist/eval-core/resume-compatibility.js +141 -0
  85. package/dist/eval-core/sample-fingerprint.d.ts +12 -0
  86. package/dist/eval-core/sample-fingerprint.js +193 -0
  87. package/dist/eval-core/schema.js +86 -31
  88. package/dist/eval-core/verdict.d.ts +8 -4
  89. package/dist/eval-core/verdict.js +24 -10
  90. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
  91. package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
  92. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
  93. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
  94. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
  95. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
  96. package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
  97. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
  98. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
  99. package/dist/eval-workflows/evaluation-pipeline.js +12 -7
  100. package/dist/eval-workflows/run-evaluation.d.ts +9 -7
  101. package/dist/eval-workflows/run-evaluation.js +79 -51
  102. package/dist/executors/anthropic-api.js +65 -9
  103. package/dist/executors/claude-cli.js +16 -79
  104. package/dist/executors/claude-protocol.d.ts +28 -0
  105. package/dist/executors/claude-protocol.js +180 -0
  106. package/dist/executors/claude-sdk-trace.js +56 -28
  107. package/dist/executors/claude-sdk.d.ts +1 -0
  108. package/dist/executors/claude-sdk.js +39 -93
  109. package/dist/executors/codex-cli-trace.js +166 -31
  110. package/dist/executors/codex-cli.d.ts +6 -8
  111. package/dist/executors/codex-cli.js +49 -151
  112. package/dist/executors/codex-protocol.d.ts +24 -0
  113. package/dist/executors/codex-protocol.js +234 -0
  114. package/dist/executors/codex-sdk.js +68 -120
  115. package/dist/executors/gemini.js +88 -13
  116. package/dist/executors/index.d.ts +2 -3
  117. package/dist/executors/index.js +5 -3
  118. package/dist/executors/openai-api.js +70 -9
  119. package/dist/executors/runtime-fingerprint.js +88 -11
  120. package/dist/executors/script-command.d.ts +8 -0
  121. package/dist/executors/script-command.js +87 -0
  122. package/dist/executors/script.js +202 -29
  123. package/dist/executors/shared.d.ts +35 -3
  124. package/dist/executors/shared.js +113 -15
  125. package/dist/grading/assertions.d.ts +1 -1
  126. package/dist/grading/assertions.js +19 -9
  127. package/dist/grading/diagnostic.d.ts +9 -2
  128. package/dist/grading/diagnostic.js +25 -2
  129. package/dist/grading/index.js +10 -4
  130. package/dist/grading/judge.js +19 -6
  131. package/dist/grading/layered-scores.d.ts +2 -3
  132. package/dist/grading/layered-scores.js +2 -3
  133. package/dist/inputs/load-samples.d.ts +1 -2
  134. package/dist/inputs/load-samples.js +23 -1
  135. package/dist/inputs/mcp-resolver.js +6 -3
  136. package/dist/inputs/sample-document.d.ts +11 -0
  137. package/dist/inputs/sample-document.js +96 -0
  138. package/dist/managed/evidence.d.ts +1 -0
  139. package/dist/managed/evidence.js +1 -1
  140. package/dist/managed/store.js +200 -91
  141. package/dist/observability/codex-trace-adapter.d.ts +5 -0
  142. package/dist/observability/codex-trace-adapter.js +850 -0
  143. package/dist/observability/experience.d.ts +32 -6
  144. package/dist/observability/experience.js +2695 -459
  145. package/dist/observability/feedback-matchers.js +16 -1
  146. package/dist/observability/inbox-view-model.d.ts +2 -1
  147. package/dist/observability/inbox-view-model.js +20 -14
  148. package/dist/observability/inbox.d.ts +7 -1
  149. package/dist/observability/inbox.js +632 -124
  150. package/dist/observability/problem-patterns.js +2 -0
  151. package/dist/observability/review-state.d.ts +6 -0
  152. package/dist/observability/review-state.js +235 -63
  153. package/dist/observability/skill-chain-advisories.js +1 -1
  154. package/dist/observability/skill-chain.js +17 -4
  155. package/dist/observability/skill-health-analyzer.d.ts +32 -7
  156. package/dist/observability/skill-health-analyzer.js +194 -121
  157. package/dist/observability/skill-health-report.d.ts +10 -0
  158. package/dist/observability/skill-health-report.js +620 -0
  159. package/dist/observability/soft-standards/constants.d.ts +0 -1
  160. package/dist/observability/soft-standards/constants.js +0 -1
  161. package/dist/observability/soft-standards/index.d.ts +1 -1
  162. package/dist/observability/soft-standards/index.js +1 -1
  163. package/dist/observability/soft-standards/llm-extractor.js +8 -10
  164. package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
  165. package/dist/observability/soft-standards/skill-standards-store.js +59 -18
  166. package/dist/observability/soft-standards/types.d.ts +2 -2
  167. package/dist/observability/trace-adapter.d.ts +12 -7
  168. package/dist/observability/trace-adapter.js +11 -9
  169. package/dist/observability/trace-attribution.d.ts +13 -5
  170. package/dist/observability/trace-attribution.js +315 -21
  171. package/dist/observability/trace-ingestion.d.ts +9 -0
  172. package/dist/observability/trace-ingestion.js +80 -0
  173. package/dist/observability/trace-ir.d.ts +113 -0
  174. package/dist/observability/trace-ir.js +87 -0
  175. package/dist/observability/trace-segmenter.d.ts +19 -6
  176. package/dist/observability/trace-segmenter.js +377 -196
  177. package/dist/observability/trace-session-index.d.ts +19 -0
  178. package/dist/observability/trace-session-index.js +68 -0
  179. package/dist/observability/trace-source.d.ts +12 -4
  180. package/dist/observability/trace-source.js +939 -215
  181. package/dist/renderer/html-renderer.js +37 -6
  182. package/dist/renderer/icons.js +3 -0
  183. package/dist/renderer/observation-inbox-renderer.js +227 -91
  184. package/dist/renderer/skill-detail-renderer.js +452 -109
  185. package/dist/renderer/skill-health-renderer.js +69 -12
  186. package/dist/renderer/summary.js +28 -7
  187. package/dist/renderer/table.js +21 -4
  188. package/dist/renderer/test-view.d.ts +1 -0
  189. package/dist/renderer/test-view.js +44 -9
  190. package/dist/server/indexed-report-store.js +14 -18
  191. package/dist/server/job-store.js +64 -26
  192. package/dist/server/report-server.js +190 -78
  193. package/dist/server/report-store.js +57 -80
  194. package/dist/server/skill-index.js +143 -49
  195. package/dist/server/skill-insights.js +44 -5
  196. package/dist/shared/artifact-graph.d.ts +3 -0
  197. package/dist/shared/artifact-graph.js +224 -0
  198. package/dist/shared/assertion-types.d.ts +8 -0
  199. package/dist/shared/assertion-types.js +46 -0
  200. package/dist/shared/atomic-json.d.ts +8 -0
  201. package/dist/shared/atomic-json.js +33 -0
  202. package/dist/shared/diagnosis-schema.d.ts +9 -0
  203. package/dist/shared/diagnosis-schema.js +181 -0
  204. package/dist/shared/doctor-report.d.ts +3 -0
  205. package/dist/shared/doctor-report.js +103 -0
  206. package/dist/shared/evaluation-job.d.ts +6 -0
  207. package/dist/shared/evaluation-job.js +217 -0
  208. package/dist/shared/executor-result.d.ts +17 -0
  209. package/dist/shared/executor-result.js +221 -0
  210. package/dist/shared/file-lock.d.ts +12 -0
  211. package/dist/shared/file-lock.js +129 -0
  212. package/dist/shared/json-value.d.ts +5 -0
  213. package/dist/shared/json-value.js +36 -0
  214. package/dist/shared/keyed-mutex.d.ts +7 -0
  215. package/dist/shared/keyed-mutex.js +24 -0
  216. package/dist/shared/record-count.d.ts +8 -0
  217. package/dist/shared/record-count.js +43 -0
  218. package/dist/shared/sample-contract.d.ts +3 -0
  219. package/dist/shared/sample-contract.js +332 -0
  220. package/dist/shared/shell-quote.d.ts +2 -0
  221. package/dist/shared/shell-quote.js +7 -0
  222. package/dist/shared/timestamp.d.ts +6 -0
  223. package/dist/shared/timestamp.js +64 -0
  224. package/dist/shared/token-usage.d.ts +19 -0
  225. package/dist/shared/token-usage.js +50 -0
  226. package/dist/shared/tool-call-status.d.ts +8 -0
  227. package/dist/shared/tool-call-status.js +28 -0
  228. package/dist/shared/tool-identity.d.ts +21 -0
  229. package/dist/shared/tool-identity.js +84 -0
  230. package/dist/shared/tool-search.js +73 -16
  231. package/dist/shared/trace-projection.d.ts +5 -0
  232. package/dist/shared/trace-projection.js +20 -0
  233. package/dist/shared/trace-source-kind.d.ts +3 -0
  234. package/dist/shared/trace-source-kind.js +12 -0
  235. package/dist/types/diagnosis.d.ts +2 -0
  236. package/dist/types/eval.d.ts +4 -0
  237. package/dist/types/executor.d.ts +32 -5
  238. package/dist/types/index.d.ts +1 -0
  239. package/dist/types/index.js +1 -0
  240. package/dist/types/judge.d.ts +2 -0
  241. package/dist/types/observability.d.ts +116 -9
  242. package/dist/types/report.d.ts +58 -6
  243. package/dist/types/skill-index.d.ts +7 -0
  244. package/dist/types/trace.d.ts +2 -0
  245. package/dist/types/trace.js +1 -0
  246. package/package.json +9 -5
@@ -1,13 +1,21 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { basename } from 'node:path';
3
+ import { isTraceSourceKind as isObservationSourceKind } from '../shared/trace-source-kind.js';
2
4
  import { buildExperienceProblemPatterns, mergeExperienceProblemPatterns, } from './problem-patterns.js';
3
5
  import { observationMetricAnnotationVerdict, observationReviewStateKey } from './review-state.js';
6
+ import { correlateTraceToolEvents, normalizeTraceTimestamp, } from './trace-ir.js';
7
+ import { skillSegmentTimestampObserved, UNOBSERVED_TRACE_TIMESTAMP, } from './trace-segmenter.js';
8
+ import { createTraceSessionIndex, traceSessionRefIdentity } from './trace-session-index.js';
4
9
  import { extractCommandEnvelopeText, stripCommandEnvelopeText } from './trace-attribution.js';
5
- import { hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, isAssistantProgressUpdateText, isAssistantProtocolReplyText, isRuntimeProtocolPromptText, isSyntheticUserMessageText, isToolResultFailureText, isUserInteractionMetricText } from './text-signals.js';
10
+ import { hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, isAssistantProgressUpdateText, isAssistantProtocolReplyText, isSyntheticUserMessageText, isToolResultFailureText, isUserInteractionMetricText } from './text-signals.js';
6
11
  import { durationMsBetween } from '../shared/time.js';
12
+ import { incrementRecordCount, ownRecordValue, sumRecordCounts as sumSafeCounts, } from '../shared/record-count.js';
13
+ import { checkedSumTokenCounts } from '../shared/token-usage.js';
7
14
  import { loadExpectedToolsForSkill, loadFrontmatterSkillType, loadSkillDeclarationCheck, } from './experience-frontmatter.js';
8
15
  import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
9
16
  export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
10
- export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
17
+ export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 3;
18
+ const LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
11
19
  export function aggregateExperienceChecklistItemStatus(statuses) {
12
20
  if (statuses.includes('degraded'))
13
21
  return 'degraded';
@@ -41,53 +49,63 @@ const ZERO_INDICATORS = {
41
49
  repeatedExecutionCount: 0,
42
50
  toolCallCount: 0,
43
51
  toolFailureCount: 0,
52
+ toolCancelledCount: 0,
53
+ toolUnknownCount: 0,
44
54
  highObservationCount: 0,
45
55
  mediumObservationCount: 0,
46
56
  hedgingCount: 0,
47
57
  explicitMarkerCount: 0,
48
58
  };
49
59
  export function buildObservationExperienceReport(input) {
50
- const sessionsBySourceTrace = new Map(input.sessions.map((session) => [session.sourcePath, session]));
51
- const sessionGroupsById = groupSessionsByLogicalId(input.sessions);
52
- const sessionGroupsByKey = groupSessionsByExperienceKey(input.sessions);
60
+ const traceSessions = input.sessions.map((session) => ({
61
+ ...session,
62
+ events: correlateTraceToolEvents(session.events),
63
+ }));
64
+ const sessionIndex = createTraceSessionIndex(traceSessions);
65
+ const sessionGroupsByKey = groupSessionsByExperienceKey(traceSessions);
53
66
  const goalSlices = [];
54
67
  const invocations = [];
55
68
  for (const segment of input.segments) {
56
69
  if (segment.skillName === 'general')
57
70
  continue;
58
- const session = sessionsBySourceTrace.get(segment.sourceTrace ?? '') ?? sessionGroupsById.get(segment.sessionId)?.[0];
71
+ const session = sessionIndex.resolve(segment);
59
72
  const sourceTrace = segment.sourceTrace ?? session?.sourcePath ?? '';
60
73
  const sessionGroupKey = session ? experienceSessionGroupKey(session) : `trace:${sourceTrace || segment.sessionId}`;
61
74
  const relatedItems = relatedObservationItems(segment, input.items);
62
75
  const bounds = session ? segmentRecordBounds(session, segment) : { start: 0, end: 0 };
63
- const timeline = session ? buildTimeline(session, bounds.start, bounds.end) : [];
76
+ const timeline = session ? buildInvocationTimeline(session, bounds.start, bounds.end, segment) : [];
77
+ const metricTimeline = metricTimelineForSegment(timeline, segment);
64
78
  const metricScopeId = hashParts('session', segment.skillName, sessionGroupKey);
65
79
  const userRefs = timeline.filter((event) => event.kind === 'user_message');
66
- const indicators = indicatorsForSegment(segment, relatedItems, timeline, metricScopeId, input.reviewState);
80
+ const indicators = indicatorsForSegment(segment, relatedItems, metricTimeline, metricScopeId, input.reviewState);
67
81
  const observationRefs = relatedItems.map(observationEvidenceRef);
68
- const evidenceChain = evidenceChainForTimeline(timeline, observationRefs);
69
- const ruleFindings = ruleFindingsForEvidence(indicators, timeline, observationRefs, evidenceChain, metricScopeId, input.reviewState);
82
+ const evidenceChain = evidenceChainForTimeline(metricTimeline, observationRefs);
83
+ const ruleFindings = ruleFindingsForEvidence(indicators, metricTimeline, observationRefs, evidenceChain, metricScopeId, input.reviewState);
70
84
  const assistiveInference = assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings);
71
85
  const problemPatterns = buildExperienceProblemPatterns({
72
86
  skillName: segment.skillName,
73
- sessionId: segment.sessionId,
74
- timeline,
87
+ sessionId: sessionGroupKey,
88
+ timeline: metricTimeline,
75
89
  metricScopeId,
76
90
  reviewState: input.reviewState,
77
91
  });
78
92
  const hasGoalShift = userRefs.some((ref) => hasUserGoalShiftSignal(ref.snippet ?? ''));
79
- // 包含 sourceTrace 防止 main + subagent 因 segmentIndex 各自从 0 计数而撞 hash
80
- // (segmenter 给 segment.sessionId = sessionGroupId,main 和 subagent 共享)
81
- const goalSliceId = hashParts('goal', segment.sessionId, sourceTrace, segment.skillName, String(segment.segmentIndex));
82
- const invocationId = hashParts('invocation', segment.sessionId, sourceTrace, segment.skillName, String(segment.segmentIndex));
93
+ // traceId 是物理证据流身份。sourcePath 不是身份:一个 Markdown 文件可包含多个
94
+ // session,且不同 block 可能复用同一来源 sessionId。
95
+ const evidenceStreamId = traceSessionRefIdentity(segment);
96
+ const traceId = segment.traceId ?? session?.traceId ?? evidenceStreamId;
97
+ const goalSliceId = hashParts('goal', evidenceStreamId, segment.skillName, String(segment.segmentIndex));
98
+ const invocationId = hashParts('invocation', evidenceStreamId, segment.skillName, String(segment.segmentIndex));
83
99
  goalSlices.push({
84
100
  id: goalSliceId,
85
101
  skillName: segment.skillName,
86
102
  sessionId: segment.sessionId,
103
+ traceId,
87
104
  sourceTrace,
88
105
  cwd: segment.cwd,
89
106
  startTimestamp: segment.startTimestamp,
90
107
  endTimestamp: segment.endTimestamp,
108
+ timestampObserved: skillSegmentTimestampObserved(segment),
91
109
  sliceReasonCode: hasGoalShift ? 'explicit_user_goal_shift' : 'skill_segment_boundary',
92
110
  sliceConfidence: hasGoalShift ? 'medium' : 'high',
93
111
  inferredUserGoal: inferUserGoal(userRefs),
@@ -98,15 +116,17 @@ export function buildObservationExperienceReport(input) {
98
116
  skillName: segment.skillName,
99
117
  sessionId: segment.sessionId,
100
118
  sessionGroupKey,
119
+ traceId,
101
120
  sourceTrace,
102
- sourceKind: segment.sourceKind ?? sourceKindForPath(sourceTrace),
103
- entrypoint: session ? session.entrypoint ?? inferEntrypointFromRecords(session) : undefined,
104
- sourceMetadata: session?.sourceMetadata,
121
+ sourceKind: segment.sourceKind ?? session?.sourceKind ?? 'unknown',
122
+ entrypoint: session?.entrypoint,
123
+ sourceMetadata: segment.sourceMetadata ?? session?.sourceMetadata,
105
124
  cwd: segment.cwd,
106
125
  segmentIndex: segment.segmentIndex,
107
126
  goalSliceId,
108
127
  startTimestamp: segment.startTimestamp,
109
128
  endTimestamp: segment.endTimestamp,
129
+ timestampObserved: skillSegmentTimestampObserved(segment),
110
130
  attribution: {
111
131
  source: segment.attribution?.source ?? 'unknown',
112
132
  confidence: segment.attribution?.confidence ?? 0.3,
@@ -126,11 +146,15 @@ export function buildObservationExperienceReport(input) {
126
146
  ...observationRefs,
127
147
  ...userRefs.slice(0, 5).map(evidenceRefFromTimeline),
128
148
  ],
149
+ timelineRef: timelineRefForSessionGroup(sessionGroupKey),
150
+ timelineEventIds: timeline.map((event) => event.id),
129
151
  timeline,
130
152
  });
131
153
  }
132
154
  const sessions = summarizeExperienceSessions(invocations, sessionGroupsByKey, input.generatedAt, input.reviewState);
133
155
  const skills = summarizeExperienceSkills(sessions, invocations);
156
+ const traceTimelines = traceTimelinesFromSessions(sessions, invocations);
157
+ const storyContexts = storyContextsFromSessions(sessions, invocations);
134
158
  return {
135
159
  kind: 'observe-experience',
136
160
  schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
@@ -144,6 +168,8 @@ export function buildObservationExperienceReport(input) {
144
168
  noteCodes: ['no_llm_judge', 'no_auto_verdict', 'default_goal_slice_is_allowed', 'deterministic_assistive_inference'],
145
169
  },
146
170
  goalSlices,
171
+ traceTimelines,
172
+ storyContexts,
147
173
  invocations,
148
174
  sessions,
149
175
  skills,
@@ -156,54 +182,2043 @@ export function normalizeObservationExperienceReport(value) {
156
182
  const kind = report.kind === 'observe-experience' ? report.kind : null;
157
183
  if (!kind)
158
184
  return null;
159
- if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION)
185
+ if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION
186
+ && report.schemaVersion !== LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION)
160
187
  return null;
188
+ const isLegacyReport = report.schemaVersion === LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION;
161
189
  if (report.scope !== 'evidence-only')
162
190
  return null;
163
- if (typeof report.generatedAt !== 'string' || !report.meta || typeof report.meta !== 'object')
191
+ if (!isTimestamp(report.generatedAt)
192
+ || !report.meta
193
+ || typeof report.meta !== 'object'
194
+ || !isExperienceMeta(report.meta))
164
195
  return null;
165
196
  if (!Array.isArray(report.goalSlices) || !Array.isArray(report.invocations) || !Array.isArray(report.sessions) || !Array.isArray(report.skills)) {
166
197
  return null;
167
198
  }
168
- return {
199
+ const invocations = normalizeExperienceInvocationShells(report.invocations, !isLegacyReport);
200
+ const sessions = normalizeExperienceSessionShells(report.sessions, !isLegacyReport);
201
+ if (!invocations || !sessions)
202
+ return null;
203
+ if (!isLegacyReport
204
+ && (!Array.isArray(report.traceTimelines) || !Array.isArray(report.storyContexts)))
205
+ return null;
206
+ const traceTimelines = Array.isArray(report.traceTimelines)
207
+ ? normalizeTraceTimelines(report.traceTimelines)
208
+ : traceTimelinesFromSessions(sessions, invocations);
209
+ const storyContexts = Array.isArray(report.storyContexts)
210
+ ? normalizeStoryContexts(report.storyContexts)
211
+ : storyContextsFromSessions(sessions, invocations);
212
+ if (!traceTimelines || !storyContexts)
213
+ return null;
214
+ const normalized = {
169
215
  kind: 'observe-experience',
170
216
  schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
171
217
  scope: 'evidence-only',
172
218
  generatedAt: report.generatedAt,
173
219
  meta: report.meta,
174
220
  goalSlices: report.goalSlices,
175
- invocations: report.invocations,
176
- sessions: report.sessions,
221
+ traceTimelines,
222
+ storyContexts,
223
+ invocations,
224
+ sessions,
177
225
  skills: report.skills,
178
226
  };
227
+ const hydrated = hydrateExperienceTimelines(normalized);
228
+ try {
229
+ if (!validateExperienceReferences(hydrated, !isLegacyReport))
230
+ return null;
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ return hydrated;
236
+ }
237
+ function normalizeExperienceInvocationShells(values, strict) {
238
+ const records = values.filter(isObjectRecord);
239
+ if (records.length !== values.length)
240
+ return null;
241
+ if (records.some((value) => typeof value.id !== 'string'
242
+ || typeof value.skillName !== 'string'
243
+ || typeof value.sessionId !== 'string'
244
+ || typeof value.sessionGroupKey !== 'string'
245
+ || (strict ? typeof value.traceId !== 'string' : !isOptionalString(value.traceId))
246
+ || typeof value.sourceTrace !== 'string'
247
+ || !isObservationSourceKind(value.sourceKind)
248
+ || !isOptionalString(value.entrypoint)
249
+ || !isOptionalTraceSourceMetadata(value.sourceMetadata)
250
+ || !isOptionalString(value.cwd)
251
+ || !isNonNegativeInteger(value.segmentIndex)
252
+ || typeof value.goalSliceId !== 'string'
253
+ || !isTimestampRange(value.startTimestamp, value.endTimestamp)
254
+ || (value.timestampObserved !== undefined && typeof value.timestampObserved !== 'boolean')
255
+ || (strict && typeof value.timestampObserved !== 'boolean')
256
+ || !isExperienceAttribution(value.attribution)
257
+ || !isExperienceInvocationMetrics(value.metrics, strict)
258
+ || !isCountRecord(value.toolCounts)
259
+ || !isExperienceIndicators(value.indicators, strict)
260
+ || !isExperienceEvidenceChain(value.evidenceChain)
261
+ || !isExperienceRuleFindingArray(value.ruleFindings)
262
+ || !isExperienceAssistiveInference(value.assistiveInference)
263
+ || !isExperienceProblemPatternArray(value.problemPatterns)
264
+ || !isStringArray(value.relatedObservationIds)
265
+ || !isExperienceEvidenceRefArray(value.evidenceRefs)
266
+ || (strict
267
+ ? typeof value.timelineRef !== 'string'
268
+ || !isStringArray(value.timelineEventIds)
269
+ || (value.timeline !== undefined && !isTimelineEventArray(value.timeline))
270
+ : !isOptionalString(value.timelineRef)
271
+ || (value.timelineEventIds !== undefined && !isStringArray(value.timelineEventIds))
272
+ || !isTimelineEventArray(value.timeline))))
273
+ return null;
274
+ return records.map((value) => ({
275
+ ...value,
276
+ metrics: {
277
+ ...value.metrics,
278
+ tokenUsageObserved: strict
279
+ ? value.metrics.tokenUsageObserved
280
+ : false,
281
+ },
282
+ timeline: isTimelineEventArray(value.timeline)
283
+ ? value.timeline
284
+ : [],
285
+ }));
286
+ }
287
+ function normalizeExperienceSessionShells(values, strict) {
288
+ const records = values.filter(isObjectRecord);
289
+ if (records.length !== values.length)
290
+ return null;
291
+ if (records.some((value) => typeof value.id !== 'string'
292
+ || typeof value.skillName !== 'string'
293
+ || typeof value.sessionId !== 'string'
294
+ || typeof value.sourceTrace !== 'string'
295
+ || !isObservationSourceKind(value.sourceKind)
296
+ || !isOptionalString(value.entrypoint)
297
+ || !isOptionalTraceSourceMetadata(value.sourceMetadata)
298
+ || !isOptionalString(value.cwd)
299
+ || !isConsistentSourceSessionTime(value)
300
+ || !isTimestampRange(value.startTimestamp, value.endTimestamp)
301
+ || !isStringArray(value.invocationIds)
302
+ || !isOptionalTimestampCoverage(value.timestampedInvocationCount, value.timestampCoverage, Array.isArray(value.invocationIds) ? value.invocationIds.length : -1)
303
+ || (strict
304
+ && (!isNonNegativeInteger(value.timestampedInvocationCount)
305
+ || !isRate(value.timestampCoverage)))
306
+ || !isStringArray(value.goalSliceIds)
307
+ || !isExperienceReviewPriority(value.reviewPriority)
308
+ || typeof value.reviewPriorityScore !== 'number'
309
+ || !Number.isFinite(value.reviewPriorityScore)
310
+ || value.reviewPriorityScore < 0
311
+ || !isEnumArray(value.reviewBasisCodes, [
312
+ 'has_high_observation',
313
+ 'has_medium_observation',
314
+ 'user_correction',
315
+ 'user_interruption',
316
+ 'session_interrupted',
317
+ 'negative_feedback',
318
+ 'hard_rule_text_hit',
319
+ 'tool_failure',
320
+ 'hedging_signal',
321
+ 'explicit_marker',
322
+ ])
323
+ || !isExperienceIndicators(value.indicators, strict)
324
+ || !isExperienceEvidenceChain(value.evidenceChain)
325
+ || !isExperienceRuleFindingArray(value.ruleFindings)
326
+ || !isExperienceAssistiveInference(value.assistiveInference)
327
+ || !isExperienceProblemPatternArray(value.problemPatterns)
328
+ || !isStringArray(value.relatedObservationIds)
329
+ || !(strict
330
+ ? isExperienceTimelineScope(value.timelineScope)
331
+ : isLegacyExperienceTimelineScope(value.timelineScope))
332
+ || !isStringArray(value.attributionSources)
333
+ || !isStringArray(value.pluginNames)
334
+ || !isStringArray(value.rawSkillRefs)
335
+ || !isStringArray(value.commandNames)
336
+ || (strict
337
+ ? typeof value.timelineRef !== 'string'
338
+ || !isStringArray(value.timelinePreviewEventIds)
339
+ || (value.timelinePreview !== undefined && !isTimelineEventArray(value.timelinePreview))
340
+ || (value.fullSessionTimeline !== undefined && !isTimelineEventArray(value.fullSessionTimeline))
341
+ : !isOptionalString(value.timelineRef)
342
+ || (value.timelinePreviewEventIds !== undefined
343
+ && !isStringArray(value.timelinePreviewEventIds))
344
+ || !isTimelineEventArray(value.timelinePreview)
345
+ || !isTimelineEventArray(value.fullSessionTimeline))
346
+ || (value.timelineTree !== undefined && !isTimelineTree(value.timelineTree))
347
+ || (value.sessionStory !== undefined
348
+ && !isExperienceSessionStory(value.sessionStory, strict))
349
+ || (value.reviewerReport !== undefined
350
+ && !isExperienceReviewerReport(value.reviewerReport, strict))))
351
+ return null;
352
+ return records.map((value) => {
353
+ const sessionStory = isObjectRecord(value.sessionStory)
354
+ ? {
355
+ ...value.sessionStory,
356
+ goalSlices: Array.isArray(value.sessionStory.goalSlices) ? value.sessionStory.goalSlices : [],
357
+ subagentDispatches: Array.isArray(value.sessionStory.subagentDispatches)
358
+ ? value.sessionStory.subagentDispatches
359
+ : [],
360
+ episodes: Array.isArray(value.sessionStory.episodes) ? value.sessionStory.episodes : [],
361
+ }
362
+ : undefined;
363
+ return {
364
+ ...value,
365
+ invocationIds: Array.isArray(value.invocationIds) ? value.invocationIds : [],
366
+ timelinePreview: Array.isArray(value.timelinePreview) ? value.timelinePreview : [],
367
+ fullSessionTimeline: Array.isArray(value.fullSessionTimeline) ? value.fullSessionTimeline : [],
368
+ sessionStory,
369
+ };
370
+ });
371
+ }
372
+ function normalizeTraceTimelines(values) {
373
+ if (values.some((value) => {
374
+ if (!isObjectRecord(value)
375
+ || typeof value.id !== 'string'
376
+ || typeof value.sessionGroupKey !== 'string'
377
+ || typeof value.sessionId !== 'string'
378
+ || !isNonNegativeInteger(value.eventCount))
379
+ return true;
380
+ return !isTimelineTree(value.tree);
381
+ }))
382
+ return null;
383
+ return values;
384
+ }
385
+ function normalizeStoryContexts(values) {
386
+ if (values.some((value) => !isExperienceStoryContext(value)))
387
+ return null;
388
+ return values;
389
+ }
390
+ function isObjectRecord(value) {
391
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
392
+ }
393
+ function isNonNegativeInteger(value) {
394
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
395
+ }
396
+ function isRate(value) {
397
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
398
+ }
399
+ function isOptionalTimestampCoverage(count, coverage, total) {
400
+ if (count === undefined && coverage === undefined)
401
+ return true;
402
+ if (total < 0
403
+ || !isNonNegativeInteger(count)
404
+ || count > total
405
+ || !isRate(coverage))
406
+ return false;
407
+ return coverage === (total > 0 ? count / total : 0);
408
+ }
409
+ function isStringArray(value) {
410
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
411
+ }
412
+ function isOptionalString(value) {
413
+ return value === undefined || typeof value === 'string';
414
+ }
415
+ function isTimestamp(value) {
416
+ return typeof value === 'string' && normalizeTraceTimestamp(value) !== undefined;
417
+ }
418
+ function isOptionalTimestamp(value) {
419
+ return value === undefined || isTimestamp(value);
420
+ }
421
+ function isTimestampRange(start, end) {
422
+ if (!isTimestamp(start) || !isTimestamp(end))
423
+ return false;
424
+ return Date.parse(start) <= Date.parse(end);
425
+ }
426
+ function invocationTimestampObserved(invocation) {
427
+ return invocation.timestampObserved
428
+ ?? invocation.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
429
+ }
430
+ function sessionTimestampedInvocationCount(session) {
431
+ return session.timestampedInvocationCount
432
+ ?? (session.startTimestamp === UNOBSERVED_TRACE_TIMESTAMP
433
+ ? 0
434
+ : session.invocationIds.length);
435
+ }
436
+ function isConsistentSourceSessionTime(value) {
437
+ const start = value.sourceSessionStartTimestamp;
438
+ const end = value.sourceSessionEndTimestamp;
439
+ const duration = value.sourceSessionDurationMs;
440
+ if (!isOptionalTimestamp(start) || !isOptionalTimestamp(end))
441
+ return false;
442
+ if (duration !== undefined && !isNonNegativeInteger(duration))
443
+ return false;
444
+ if (start === undefined || end === undefined)
445
+ return duration === undefined;
446
+ return typeof start === 'string'
447
+ && typeof end === 'string'
448
+ && isTimestampRange(start, end)
449
+ && duration === durationMsBetween(start, end);
450
+ }
451
+ function isOptionalNonNegativeInteger(value) {
452
+ return value === undefined || isNonNegativeInteger(value);
453
+ }
454
+ function isEnumValue(value, values) {
455
+ return typeof value === 'string' && values.includes(value);
456
+ }
457
+ function isEnumArray(value, values) {
458
+ return Array.isArray(value) && value.every((item) => isEnumValue(item, values));
459
+ }
460
+ function isExperienceMeta(value) {
461
+ if (!isObjectRecord(value)
462
+ || !isNonNegativeInteger(value.sessionCount)
463
+ || !isNonNegativeInteger(value.skillCount)
464
+ || !isNonNegativeInteger(value.invocationCount)
465
+ || !isNonNegativeInteger(value.goalSliceCount))
466
+ return false;
467
+ return isEnumArray(value.noteCodes, [
468
+ 'no_llm_judge',
469
+ 'no_auto_verdict',
470
+ 'default_goal_slice_is_allowed',
471
+ 'deterministic_assistive_inference',
472
+ ]);
473
+ }
474
+ function isOptionalTraceSourceMetadata(value) {
475
+ if (value === undefined)
476
+ return true;
477
+ return isObjectRecord(value)
478
+ && isOptionalString(value.channel)
479
+ && isOptionalString(value.sender)
480
+ && isOptionalString(value.senderId)
481
+ && isOptionalString(value.provider)
482
+ && isOptionalString(value.model)
483
+ && isOptionalString(value.modelApi)
484
+ && (value.businessActions === undefined || isStringArray(value.businessActions));
485
+ }
486
+ function isExperienceReviewPriority(value) {
487
+ return value === 'review_first' || value === 'sample_review' || value === 'routine_sample';
488
+ }
489
+ function isExperienceEvidenceKind(value) {
490
+ return isEnumValue(value, [
491
+ 'user_message',
492
+ 'synthetic_user_event',
493
+ 'assistant_message',
494
+ 'tool_use',
495
+ 'tool_result',
496
+ 'skill_context',
497
+ 'runtime_context',
498
+ 'observation',
499
+ ]);
500
+ }
501
+ function isExperienceEvidenceRef(value) {
502
+ if (!isObjectRecord(value)
503
+ || typeof value.id !== 'string'
504
+ || !isExperienceEvidenceKind(value.kind)
505
+ || typeof value.sourceTrace !== 'string'
506
+ || typeof value.sessionId !== 'string'
507
+ || !isOptionalNonNegativeInteger(value.messageIndex)
508
+ || !isOptionalNonNegativeInteger(value.logicalMessageIndex)
509
+ || !isOptionalNonNegativeInteger(value.sourceLineIndex))
510
+ return false;
511
+ if (value.traceRole !== undefined
512
+ && !isEnumValue(value.traceRole, ['standalone', 'main', 'subagent']))
513
+ return false;
514
+ if (value.role !== undefined
515
+ && !isEnumValue(value.role, ['user', 'assistant', 'tool', 'other']))
516
+ return false;
517
+ return [
518
+ value.traceLabel,
519
+ value.traceId,
520
+ value.messageUuid,
521
+ value.callInstanceId,
522
+ value.toolUseId,
523
+ value.label,
524
+ value.snippet,
525
+ ].every(isOptionalString)
526
+ && isOptionalTimestamp(value.timestamp);
527
+ }
528
+ function isExperienceEvidenceRefArray(value) {
529
+ return Array.isArray(value) && value.every(isExperienceEvidenceRef);
530
+ }
531
+ function isExperienceGoalSlice(value) {
532
+ return isObjectRecord(value)
533
+ && typeof value.id === 'string'
534
+ && typeof value.skillName === 'string'
535
+ && typeof value.sessionId === 'string'
536
+ && isOptionalString(value.traceId)
537
+ && typeof value.sourceTrace === 'string'
538
+ && isOptionalString(value.cwd)
539
+ && isTimestampRange(value.startTimestamp, value.endTimestamp)
540
+ && (value.timestampObserved === undefined || typeof value.timestampObserved === 'boolean')
541
+ && isEnumValue(value.sliceReasonCode, [
542
+ 'skill_segment_boundary',
543
+ 'explicit_user_goal_shift',
544
+ 'default_session_slice',
545
+ ])
546
+ && isEnumValue(value.sliceConfidence, ['low', 'medium', 'high'])
547
+ && isOptionalString(value.inferredUserGoal)
548
+ && isExperienceEvidenceRefArray(value.userMessageRefs);
549
+ }
550
+ function isExperienceEvidenceChain(value) {
551
+ if (!isObjectRecord(value))
552
+ return false;
553
+ const countKeys = [
554
+ 'userMessageCount',
555
+ 'runtimeContextCount',
556
+ 'skillContextCount',
557
+ 'assistantMessageCount',
558
+ 'toolUseCount',
559
+ 'toolResultCount',
560
+ 'toolFailureResultCount',
561
+ 'observationCount',
562
+ ];
563
+ if (!countKeys.every((key) => isNonNegativeInteger(value[key])))
564
+ return false;
565
+ const optionalRefs = [
566
+ value.firstUserMessage,
567
+ value.firstRuntimeContext,
568
+ value.firstSkillContext,
569
+ value.firstToolUse,
570
+ value.firstToolFailure,
571
+ value.lastAssistantMessage,
572
+ ];
573
+ return optionalRefs.every((ref) => ref === undefined || isExperienceEvidenceRef(ref));
574
+ }
575
+ function isExperienceRuleFinding(value) {
576
+ return isObjectRecord(value)
577
+ && isEnumValue(value.code, [
578
+ 'high_observation_seen',
579
+ 'medium_observation_seen',
580
+ 'user_correction_seen',
581
+ 'user_interruption_seen',
582
+ 'session_interrupted_seen',
583
+ 'negative_feedback_seen',
584
+ 'positive_feedback_seen',
585
+ 'user_goal_shift_seen',
586
+ 'hard_rule_seen',
587
+ 'tool_failure_seen',
588
+ 'hedging_seen',
589
+ 'explicit_marker_seen',
590
+ 'runtime_context_excluded',
591
+ 'skill_context_excluded',
592
+ 'no_priority_signal',
593
+ ])
594
+ && isEnumValue(value.level, ['attention', 'sample', 'normal'])
595
+ && isNonNegativeInteger(value.count)
596
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
597
+ }
598
+ function isExperienceRuleFindingArray(value) {
599
+ return Array.isArray(value) && value.every(isExperienceRuleFinding);
600
+ }
601
+ function isExperienceAssistiveInference(value) {
602
+ return isObjectRecord(value)
603
+ && value.mode === 'deterministic_rules_only'
604
+ && isEnumValue(value.code, [
605
+ 'review_recommended',
606
+ 'sample_recommended',
607
+ 'positive_signal_observed',
608
+ 'user_switched_topic_neutral',
609
+ 'no_obvious_issue_from_rules',
610
+ 'insufficient_human_context',
611
+ ])
612
+ && isEnumValue(value.confidence, ['low', 'medium', 'high'])
613
+ && isEnumArray(value.basisRuleCodes, [
614
+ 'high_observation_seen',
615
+ 'medium_observation_seen',
616
+ 'user_correction_seen',
617
+ 'user_interruption_seen',
618
+ 'session_interrupted_seen',
619
+ 'negative_feedback_seen',
620
+ 'positive_feedback_seen',
621
+ 'user_goal_shift_seen',
622
+ 'hard_rule_seen',
623
+ 'tool_failure_seen',
624
+ 'hedging_seen',
625
+ 'explicit_marker_seen',
626
+ 'runtime_context_excluded',
627
+ 'skill_context_excluded',
628
+ 'no_priority_signal',
629
+ ])
630
+ && isEnumArray(value.cautionCodes, [
631
+ 'no_llm_judge',
632
+ 'rule_only',
633
+ 'runtime_context_excluded',
634
+ 'skill_context_excluded',
635
+ 'no_human_user_message',
636
+ 'limited_timeline_window',
637
+ ])
638
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
639
+ }
640
+ function isExperienceProblemEvidenceRef(value) {
641
+ if (!isObjectRecord(value)
642
+ || typeof value.id !== 'string'
643
+ || typeof value.kind !== 'string'
644
+ || typeof value.sourceTrace !== 'string'
645
+ || typeof value.sessionId !== 'string'
646
+ || !isOptionalNonNegativeInteger(value.messageIndex)
647
+ || !isOptionalNonNegativeInteger(value.logicalMessageIndex)
648
+ || !isOptionalNonNegativeInteger(value.sourceLineIndex))
649
+ return false;
650
+ if (value.role !== undefined
651
+ && !isEnumValue(value.role, ['user', 'assistant', 'tool', 'other']))
652
+ return false;
653
+ return [
654
+ value.messageUuid,
655
+ value.traceId,
656
+ value.callInstanceId,
657
+ value.toolUseId,
658
+ value.label,
659
+ value.snippet,
660
+ ].every(isOptionalString)
661
+ && isOptionalTimestamp(value.timestamp);
662
+ }
663
+ function isExperienceProblemPattern(value) {
664
+ return isObjectRecord(value)
665
+ && typeof value.id === 'string'
666
+ && isEnumValue(value.bucket, [
667
+ 'output_format',
668
+ 'content_accuracy',
669
+ 'missing_context',
670
+ 'rule_violation',
671
+ 'workflow_mismatch',
672
+ 'tool_runtime',
673
+ 'goal_shift',
674
+ 'unclear',
675
+ ])
676
+ && typeof value.patternKey === 'string'
677
+ && isNonNegativeInteger(value.count)
678
+ && isNonNegativeInteger(value.sessionCount)
679
+ && isStringArray(value.recentSessionIds)
680
+ && isEnumArray(value.signalTypes, [
681
+ 'user_correction',
682
+ 'negative_feedback',
683
+ 'user_interruption',
684
+ 'hard_rule',
685
+ 'user_goal_shift',
686
+ 'tool_failure',
687
+ 'workflow_mismatch',
688
+ 'artifact_missing',
689
+ 'observer_lifecycle_failed',
690
+ 'orchestration_boundary_violation',
691
+ ])
692
+ && Array.isArray(value.evidenceRefs)
693
+ && value.evidenceRefs.every(isExperienceProblemEvidenceRef)
694
+ && isOptionalTimestamp(value.lastSeen);
695
+ }
696
+ function isExperienceProblemPatternArray(value) {
697
+ return Array.isArray(value) && value.every(isExperienceProblemPattern);
698
+ }
699
+ function isExperienceAttribution(value) {
700
+ return isObjectRecord(value)
701
+ && typeof value.source === 'string'
702
+ && isRate(value.confidence)
703
+ && isOptionalString(value.rawSkillRef)
704
+ && isOptionalString(value.pluginName)
705
+ && isOptionalString(value.commandName);
706
+ }
707
+ function isExperienceInvocationMetrics(value, requireSourceNeutralOutcomes = false) {
708
+ if (!isObjectRecord(value))
709
+ return false;
710
+ const fields = [
711
+ value.durationMs,
712
+ value.inputTokens,
713
+ value.outputTokens,
714
+ value.cacheReadTokens,
715
+ value.cacheCreationTokens,
716
+ value.numTurns,
717
+ value.numToolCalls,
718
+ value.numToolFailures,
719
+ ];
720
+ if (!fields.every(isNonNegativeInteger))
721
+ return false;
722
+ if (requireSourceNeutralOutcomes
723
+ && (typeof value.tokenUsageObserved !== 'boolean'
724
+ ||
725
+ !isNonNegativeInteger(value.numToolCancelled)
726
+ || !isNonNegativeInteger(value.numToolUnknown)))
727
+ return false;
728
+ if (value.tokenUsageObserved !== undefined
729
+ && typeof value.tokenUsageObserved !== 'boolean')
730
+ return false;
731
+ if (value.numToolCancelled !== undefined && !isNonNegativeInteger(value.numToolCancelled))
732
+ return false;
733
+ if (value.numToolUnknown !== undefined && !isNonNegativeInteger(value.numToolUnknown))
734
+ return false;
735
+ const cancelled = typeof value.numToolCancelled === 'number' ? value.numToolCancelled : 0;
736
+ const unknown = typeof value.numToolUnknown === 'number' ? value.numToolUnknown : 0;
737
+ return value.numToolFailures + cancelled + unknown <= value.numToolCalls;
738
+ }
739
+ const EXPERIENCE_INDICATOR_KEYS = [
740
+ 'userMessageCount',
741
+ 'userFollowUpCount',
742
+ 'userCorrectionCount',
743
+ 'userInterruptionCount',
744
+ 'sessionInterruptedCount',
745
+ 'negativeFeedbackCount',
746
+ 'positiveFeedbackCount',
747
+ 'userGoalShiftCount',
748
+ 'hardRuleTextHitCount',
749
+ 'assistantDeliverySignalCount',
750
+ 'deliverableArtifactSignalCount',
751
+ 'routerDownstreamCompleted',
752
+ 'routerDownstreamFailed',
753
+ 'selfCorrectionCount',
754
+ 'repeatedExecutionCount',
755
+ 'toolCallCount',
756
+ 'toolFailureCount',
757
+ 'highObservationCount',
758
+ 'mediumObservationCount',
759
+ 'hedgingCount',
760
+ 'explicitMarkerCount',
761
+ ];
762
+ function isExperienceIndicators(value, requireSourceNeutralOutcomes = false) {
763
+ return isObjectRecord(value)
764
+ && EXPERIENCE_INDICATOR_KEYS.every((key) => isNonNegativeInteger(value[key]))
765
+ && (!requireSourceNeutralOutcomes
766
+ || (isNonNegativeInteger(value.toolCancelledCount)
767
+ && isNonNegativeInteger(value.toolUnknownCount)))
768
+ && (value.toolCancelledCount === undefined || isNonNegativeInteger(value.toolCancelledCount))
769
+ && (value.toolUnknownCount === undefined || isNonNegativeInteger(value.toolUnknownCount))
770
+ && value.toolFailureCount
771
+ + (typeof value.toolCancelledCount === 'number' ? value.toolCancelledCount : 0)
772
+ + (typeof value.toolUnknownCount === 'number' ? value.toolUnknownCount : 0)
773
+ <= value.toolCallCount;
774
+ }
775
+ function isCountRecord(value) {
776
+ return isObjectRecord(value)
777
+ && Object.values(value).every(isNonNegativeInteger);
778
+ }
779
+ function isExperienceTimelineScope(value) {
780
+ if (!isObjectRecord(value)
781
+ || value.mode !== 'skill_segment_window'
782
+ || !isNonNegativeInteger(value.segmentEventCount)
783
+ || !isNonNegativeInteger(value.previewEventCount)
784
+ || !isNonNegativeInteger(value.fullSessionEventCount)
785
+ || !isExperienceTraceRecordRangeArray(value.segmentRecordRanges)
786
+ || !isExperienceTraceRecordRangeArray(value.previewRecordRanges)
787
+ || !isExperienceTraceRecordRangeArray(value.sessionRecordRanges)
788
+ || typeof value.truncated !== 'boolean'
789
+ || !isNonNegativeInteger(value.omittedBeforeCount)
790
+ || !isNonNegativeInteger(value.omittedAfterCount))
791
+ return false;
792
+ return value.previewEventCount <= value.segmentEventCount
793
+ && value.segmentEventCount <= value.fullSessionEventCount
794
+ && value.omittedBeforeCount + value.omittedAfterCount <= value.fullSessionEventCount;
795
+ }
796
+ function isLegacyExperienceTimelineScope(value) {
797
+ if (!isObjectRecord(value)
798
+ || value.mode !== 'skill_segment_window'
799
+ || !isOptionalNonNegativeInteger(value.segmentStartRecordIndex)
800
+ || !isOptionalNonNegativeInteger(value.segmentEndRecordIndex)
801
+ || !isOptionalNonNegativeInteger(value.previewStartRecordIndex)
802
+ || !isOptionalNonNegativeInteger(value.previewEndRecordIndex)
803
+ || !isNonNegativeInteger(value.sessionStartRecordIndex)
804
+ || !isNonNegativeInteger(value.sessionEndRecordIndex)
805
+ || !isNonNegativeInteger(value.previewEventCount)
806
+ || !isNonNegativeInteger(value.fullSessionEventCount)
807
+ || typeof value.truncated !== 'boolean'
808
+ || !isNonNegativeInteger(value.omittedBeforeCount)
809
+ || !isNonNegativeInteger(value.omittedAfterCount))
810
+ return false;
811
+ const optionalRanges = [
812
+ [value.segmentStartRecordIndex, value.segmentEndRecordIndex],
813
+ [value.previewStartRecordIndex, value.previewEndRecordIndex],
814
+ ];
815
+ if (optionalRanges.some(([start, end]) => (start === undefined) !== (end === undefined)
816
+ || (typeof start === 'number'
817
+ && typeof end === 'number'
818
+ && start > end)))
819
+ return false;
820
+ return value.sessionStartRecordIndex <= value.sessionEndRecordIndex
821
+ && value.previewEventCount <= value.fullSessionEventCount
822
+ && value.omittedBeforeCount + value.omittedAfterCount <= value.fullSessionEventCount;
823
+ }
824
+ function isExperienceTraceRecordRangeArray(value) {
825
+ if (!Array.isArray(value))
826
+ return false;
827
+ const identities = new Set();
828
+ for (const range of value) {
829
+ if (!isObjectRecord(range)
830
+ || typeof range.traceId !== 'string'
831
+ || typeof range.sourceTrace !== 'string'
832
+ || !isNonNegativeInteger(range.startRecordIndex)
833
+ || !isNonNegativeInteger(range.endRecordIndex)
834
+ || range.startRecordIndex > range.endRecordIndex
835
+ || !isNonNegativeInteger(range.eventCount)
836
+ || range.eventCount === 0
837
+ || identities.has(range.traceId))
838
+ return false;
839
+ identities.add(range.traceId);
840
+ }
841
+ return true;
842
+ }
843
+ function isExperienceSkillSummary(value, requireToolUnknown = false) {
844
+ if (!isObjectRecord(value)
845
+ || typeof value.skillName !== 'string'
846
+ || !isNonNegativeInteger(value.invocationCount)
847
+ || !isNonNegativeInteger(value.sessionCount)
848
+ || !Array.isArray(value.sourceKinds)
849
+ || !value.sourceKinds.every(isObservationSourceKind)
850
+ || !isStringArray(value.entrypoints)
851
+ || !isCountRecord(value.entrypointCounts)
852
+ || !isCountRecord(value.attributionCounts)
853
+ || !isStringArray(value.pluginNames)
854
+ || !isStringArray(value.rawSkillRefs)
855
+ || !isStringArray(value.commandNames)
856
+ || !isCountRecord(value.toolCounts)
857
+ || !isTimestampRange(value.firstSeen, value.lastSeen)
858
+ || !isOptionalTimestampCoverage(value.timestampedInvocationCount, value.timestampCoverage, value.invocationCount)
859
+ || (requireToolUnknown
860
+ && (!isNonNegativeInteger(value.timestampedInvocationCount)
861
+ || !isRate(value.timestampCoverage)))
862
+ || !isNonNegativeInteger(value.reviewFirstSessionCount)
863
+ || !isNonNegativeInteger(value.sampleReviewSessionCount)
864
+ || !isExperienceIndicators(value.indicators, requireToolUnknown)
865
+ || !isExperienceEvidenceChain(value.evidenceChain)
866
+ || !isExperienceRuleFindingArray(value.ruleFindings)
867
+ || !isExperienceAssistiveInference(value.assistiveInference)
868
+ || !isExperienceProblemPatternArray(value.problemPatterns)
869
+ || !isStringArray(value.relatedObservationIds)
870
+ || !isObjectRecord(value.sourceMetadataCounts))
871
+ return false;
872
+ const metadataCounts = value.sourceMetadataCounts;
873
+ return ['channels', 'senders', 'businessActions', 'providers', 'models']
874
+ .every((key) => isCountRecord(metadataCounts[key]));
875
+ }
876
+ function isTimelineEvent(value) {
877
+ if (!isObjectRecord(value)
878
+ || typeof value.id !== 'string'
879
+ || ![
880
+ 'user_message',
881
+ 'synthetic_user_event',
882
+ 'assistant_message',
883
+ 'tool_use',
884
+ 'tool_result',
885
+ 'skill_context',
886
+ 'runtime_context',
887
+ 'observation',
888
+ ].includes(String(value.kind))
889
+ || typeof value.sourceTrace !== 'string'
890
+ || typeof value.sessionId !== 'string'
891
+ || !isNonNegativeInteger(value.order))
892
+ return false;
893
+ const optionalIndexes = [
894
+ value.messageIndex,
895
+ value.logicalMessageIndex,
896
+ value.sourceLineIndex,
897
+ ];
898
+ if (!optionalIndexes.every((index) => index === undefined || isNonNegativeInteger(index)))
899
+ return false;
900
+ if (value.traceRole !== undefined
901
+ && value.traceRole !== 'standalone'
902
+ && value.traceRole !== 'main'
903
+ && value.traceRole !== 'subagent')
904
+ return false;
905
+ if (value.role !== undefined
906
+ && value.role !== 'user'
907
+ && value.role !== 'assistant'
908
+ && value.role !== 'tool'
909
+ && value.role !== 'other')
910
+ return false;
911
+ if (value.toolStatus !== undefined
912
+ && value.toolStatus !== 'success'
913
+ && value.toolStatus !== 'failure'
914
+ && value.toolStatus !== 'cancelled'
915
+ && value.toolStatus !== 'unknown')
916
+ return false;
917
+ const optionalStrings = [
918
+ value.traceId,
919
+ value.traceLabel,
920
+ value.messageUuid,
921
+ value.callInstanceId,
922
+ value.toolUseId,
923
+ value.label,
924
+ value.snippet,
925
+ value.toolName,
926
+ value.fullText,
927
+ ];
928
+ if (!optionalStrings.every((field) => field === undefined || typeof field === 'string')
929
+ || !isOptionalTimestamp(value.timestamp)
930
+ || (value.isError !== undefined && typeof value.isError !== 'boolean'))
931
+ return false;
932
+ return value.kind !== 'tool_result'
933
+ || value.toolStatus === undefined
934
+ || value.isError === undefined
935
+ || value.isError === (value.toolStatus === 'failure');
936
+ }
937
+ function isTimelineEventArray(value) {
938
+ return Array.isArray(value) && value.every(isTimelineEvent);
939
+ }
940
+ function isTimelineTree(value) {
941
+ if (!isObjectRecord(value)
942
+ || typeof value.sessionId !== 'string'
943
+ || !isTimelineEventArray(value.main)
944
+ || !Array.isArray(value.branches))
945
+ return false;
946
+ return value.branches.every((branch) => {
947
+ if (!isObjectRecord(branch)
948
+ || typeof branch.id !== 'string'
949
+ || typeof branch.label !== 'string'
950
+ || typeof branch.sessionId !== 'string'
951
+ || !isOptionalString(branch.traceId)
952
+ || typeof branch.sourceTrace !== 'string'
953
+ || (branch.traceRole !== 'main'
954
+ && branch.traceRole !== 'subagent'
955
+ && branch.traceRole !== 'standalone')
956
+ || !isTimelineEventArray(branch.events))
957
+ return false;
958
+ if (branch.attachTo === undefined)
959
+ return true;
960
+ if (!isObjectRecord(branch.attachTo)
961
+ || !isOptionalString(branch.attachTo.traceId)
962
+ || typeof branch.attachTo.sourceTrace !== 'string')
963
+ return false;
964
+ return (branch.attachTo.messageIndex === undefined || isNonNegativeInteger(branch.attachTo.messageIndex))
965
+ && (branch.attachTo.callInstanceId === undefined || typeof branch.attachTo.callInstanceId === 'string')
966
+ && (branch.attachTo.toolUseId === undefined || typeof branch.attachTo.toolUseId === 'string')
967
+ && (branch.attachTo.label === undefined || typeof branch.attachTo.label === 'string');
968
+ });
969
+ }
970
+ function isExperienceChecklistItem(value) {
971
+ return isObjectRecord(value)
972
+ && typeof value.key === 'string'
973
+ && typeof value.label === 'string'
974
+ && isEnumValue(value.status, [
975
+ 'passed',
976
+ 'failed',
977
+ 'unknown',
978
+ 'not_declared',
979
+ 'not_applicable',
980
+ 'degraded',
981
+ ])
982
+ && isEnumValue(value.contribution, [
983
+ 'blocking',
984
+ 'attention',
985
+ 'informational',
986
+ 'positive',
987
+ 'neutral',
988
+ ])
989
+ && typeof value.reason === 'string'
990
+ && isExperienceEvidenceRefArray(value.evidenceRefs)
991
+ && isEnumValue(value.source, ['deterministic_rule', 'llm_soft', 'manual'])
992
+ && isOptionalString(value.suggestionKey);
993
+ }
994
+ function isExperienceSessionStoryGoalSlice(value) {
995
+ return isObjectRecord(value)
996
+ && typeof value.id === 'string'
997
+ && isNonNegativeInteger(value.order)
998
+ && isStringArray(value.skillNames)
999
+ && isTimestampRange(value.startTimestamp, value.endTimestamp)
1000
+ && isEnumValue(value.reasonCode, [
1001
+ 'skill_segment_boundary',
1002
+ 'explicit_user_goal_shift',
1003
+ 'default_session_slice',
1004
+ ])
1005
+ && isOptionalString(value.inferredUserGoal)
1006
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1007
+ }
1008
+ function isExperienceSessionStorySubagentDispatch(value) {
1009
+ if (!isObjectRecord(value)
1010
+ || typeof value.id !== 'string'
1011
+ || !isNonNegativeInteger(value.order)
1012
+ || typeof value.branchId !== 'string'
1013
+ || typeof value.childSessionId !== 'string'
1014
+ || typeof value.traceId !== 'string'
1015
+ || typeof value.label !== 'string'
1016
+ || typeof value.sourceTrace !== 'string'
1017
+ || !isNonNegativeInteger(value.eventCount)
1018
+ || !isExperienceEvidenceRefArray(value.evidenceRefs))
1019
+ return false;
1020
+ if (value.attachTo === undefined)
1021
+ return true;
1022
+ return isObjectRecord(value.attachTo)
1023
+ && isOptionalNonNegativeInteger(value.attachTo.messageIndex)
1024
+ && isOptionalString(value.attachTo.callInstanceId)
1025
+ && isOptionalString(value.attachTo.toolUseId)
1026
+ && isOptionalString(value.attachTo.label);
1027
+ }
1028
+ function isExperienceSessionStorySkillLink(value) {
1029
+ return isObjectRecord(value)
1030
+ && typeof value.id === 'string'
1031
+ && isNonNegativeInteger(value.order)
1032
+ && typeof value.skillName === 'string'
1033
+ && isEnumValue(value.role, ['router', 'executor', 'mixed', 'unknown'])
1034
+ && isStringArray(value.invocationIds)
1035
+ && isStringArray(value.goalSliceIds)
1036
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1037
+ }
1038
+ function isExperienceSessionStoryGraphNode(value) {
1039
+ return isObjectRecord(value)
1040
+ && typeof value.id === 'string'
1041
+ && typeof value.label === 'string'
1042
+ && isEnumValue(value.kind, [
1043
+ 'user_goal',
1044
+ 'skill_invocation',
1045
+ 'subagent_branch',
1046
+ 'tool_execution',
1047
+ 'delivery',
1048
+ 'user_feedback',
1049
+ 'goal_shift',
1050
+ ])
1051
+ && isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
1052
+ && (value.role === undefined || isEnumValue(value.role, ['router', 'executor', 'mixed', 'unknown']))
1053
+ && isOptionalString(value.detailNodeId);
1054
+ }
1055
+ function isExperienceSessionStoryGraphEdge(value) {
1056
+ return isObjectRecord(value)
1057
+ && typeof value.fromId === 'string'
1058
+ && typeof value.toId === 'string'
1059
+ && typeof value.label === 'string';
1060
+ }
1061
+ function isExperienceSessionStoryNode(value) {
1062
+ return isObjectRecord(value)
1063
+ && typeof value.id === 'string'
1064
+ && isNonNegativeInteger(value.order)
1065
+ && isEnumValue(value.kind, [
1066
+ 'user_goal',
1067
+ 'skill_invocation',
1068
+ 'subagent_branch',
1069
+ 'tool_execution',
1070
+ 'delivery',
1071
+ 'user_feedback',
1072
+ 'goal_shift',
1073
+ ])
1074
+ && typeof value.label === 'string'
1075
+ && isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
1076
+ && typeof value.text === 'string'
1077
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1078
+ }
1079
+ function isExperienceSessionStoryAnswer(value) {
1080
+ return isObjectRecord(value)
1081
+ && isEnumValue(value.key, ['goal_satisfaction', 'declared_behavior_fit', 'user_feeling'])
1082
+ && typeof value.label === 'string'
1083
+ && isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
1084
+ && isEnumValue(value.reason, [
1085
+ 'data_degraded',
1086
+ 'blocking_failed',
1087
+ 'attention_accumulated',
1088
+ 'unknown_dominant',
1089
+ 'all_passed',
1090
+ 'not_applicable',
1091
+ ])
1092
+ && isStringArray(value.sourceItemKeys)
1093
+ && typeof value.text === 'string'
1094
+ && isExperienceEvidenceRefArray(value.evidenceRefs)
1095
+ && Array.isArray(value.checklistItems)
1096
+ && value.checklistItems.every(isExperienceChecklistItem);
1097
+ }
1098
+ function isExperienceGoalEvidenceRef(value) {
1099
+ return isObjectRecord(value)
1100
+ && isEnumValue(value.kind, ['user_message', 'goal_slice', 'llm_goal'])
1101
+ && isOptionalString(value.goalSliceId)
1102
+ && (value.evidenceRef === undefined || isExperienceEvidenceRef(value.evidenceRef))
1103
+ && isOptionalString(value.label);
1104
+ }
1105
+ function isExperienceSkillSegment(value) {
1106
+ if (!isObjectRecord(value)
1107
+ || typeof value.id !== 'string'
1108
+ || !isNonNegativeInteger(value.order)
1109
+ || typeof value.skillName !== 'string'
1110
+ || !isEnumValue(value.skillType, [
1111
+ 'router',
1112
+ 'delegation',
1113
+ 'executor',
1114
+ 'advisory',
1115
+ 'workflow_owner',
1116
+ 'unknown',
1117
+ ])
1118
+ || (value.skillTypeSource !== undefined
1119
+ && !isEnumValue(value.skillTypeSource, ['frontmatter', 'trace', 'unknown']))
1120
+ || (value.declaredSkillType !== undefined
1121
+ && !isEnumValue(value.declaredSkillType, [
1122
+ 'router',
1123
+ 'delegation',
1124
+ 'executor',
1125
+ 'advisory',
1126
+ 'workflow_owner',
1127
+ 'unknown',
1128
+ ]))
1129
+ || (value.traceInferredSkillType !== undefined
1130
+ && !isEnumValue(value.traceInferredSkillType, [
1131
+ 'router',
1132
+ 'delegation',
1133
+ 'executor',
1134
+ 'advisory',
1135
+ 'workflow_owner',
1136
+ 'unknown',
1137
+ ]))
1138
+ || !isEnumValue(value.episodeRole, [
1139
+ 'main_executor',
1140
+ 'router',
1141
+ 'delegator',
1142
+ 'supporting',
1143
+ 'observer',
1144
+ ])
1145
+ || !isStringArray(value.skillInvocationIds)
1146
+ || !isOptionalNonNegativeInteger(value.startMessageIndex)
1147
+ || !isOptionalNonNegativeInteger(value.endMessageIndex)
1148
+ || !isTimestampRange(value.startTimestamp, value.endTimestamp)
1149
+ || !Array.isArray(value.typeSpecificChecklist)
1150
+ || !value.typeSpecificChecklist.every(isExperienceChecklistItem)
1151
+ || !isExperienceEvidenceRefArray(value.evidenceRefs))
1152
+ return false;
1153
+ if (value.messageRanges !== undefined
1154
+ && (!Array.isArray(value.messageRanges)
1155
+ || value.messageRanges.some((range) => !isObjectRecord(range)
1156
+ || !isNonNegativeInteger(range.startMessageIndex)
1157
+ || !isNonNegativeInteger(range.endMessageIndex)
1158
+ || range.startMessageIndex > range.endMessageIndex
1159
+ || !isOptionalString(range.traceId)
1160
+ || !isOptionalString(range.sourceTrace)
1161
+ || !isOptionalString(range.sessionId))))
1162
+ return false;
1163
+ if (typeof value.startMessageIndex === 'number'
1164
+ && typeof value.endMessageIndex === 'number'
1165
+ && value.startMessageIndex > value.endMessageIndex)
1166
+ return false;
1167
+ if (value.runtimeAssessment === undefined)
1168
+ return true;
1169
+ return isObjectRecord(value.runtimeAssessment)
1170
+ && isOptionalString(value.runtimeAssessment.goalSatisfaction)
1171
+ && isOptionalString(value.runtimeAssessment.declaredBehaviorFit)
1172
+ && isOptionalString(value.runtimeAssessment.artifactGoalMatch)
1173
+ && isOptionalString(value.runtimeAssessment.userFeeling);
1174
+ }
1175
+ function isExperienceOrchestrationEdge(value) {
1176
+ return isObjectRecord(value)
1177
+ && typeof value.id === 'string'
1178
+ && typeof value.episodeId === 'string'
1179
+ && isEnumValue(value.edgeKind, ['internal_skill', 'external_child_session'])
1180
+ && isOptionalString(value.parentSkillSegmentId)
1181
+ && isOptionalString(value.executorSkillSegmentId)
1182
+ && isOptionalString(value.childSessionId)
1183
+ && (value.runnerStartedRef === undefined || isExperienceEvidenceRef(value.runnerStartedRef))
1184
+ && (value.runnerCompletedRef === undefined || isExperienceEvidenceRef(value.runnerCompletedRef))
1185
+ && (value.notificationRef === undefined || isExperienceEvidenceRef(value.notificationRef))
1186
+ && isEnumValue(value.status, ['started', 'completed', 'failed', 'unknown'])
1187
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1188
+ }
1189
+ function isExperienceFeedbackAttribution(value) {
1190
+ return isObjectRecord(value)
1191
+ && isOptionalString(value.skillName)
1192
+ && isOptionalString(value.skillSegmentId)
1193
+ && isEnumValue(value.attributionRole, ['primary_fault', 'downstream_related', 'context_only'])
1194
+ && isEnumValue(value.reasonCode, [
1195
+ 'object_match',
1196
+ 'promise_match',
1197
+ 'action_match',
1198
+ 'orchestration_edge',
1199
+ 'episode_context',
1200
+ ])
1201
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1202
+ }
1203
+ function isExperienceFeedbackSignal(value) {
1204
+ return isObjectRecord(value)
1205
+ && typeof value.id === 'string'
1206
+ && isNonNegativeInteger(value.order)
1207
+ && isEnumValue(value.type, [
1208
+ 'correction',
1209
+ 'follow_up',
1210
+ 'frustration',
1211
+ 'interruption',
1212
+ 'positive',
1213
+ 'unknown',
1214
+ ])
1215
+ && typeof value.text === 'string'
1216
+ && isOptionalString(value.targetObject)
1217
+ && isEnumValue(value.sourceWindow, [
1218
+ 'session',
1219
+ 'episode',
1220
+ 'skill_invocation',
1221
+ 'downstream_child',
1222
+ ])
1223
+ && isExperienceEvidenceRef(value.evidenceRef)
1224
+ && (value.canonicalAttributions === undefined
1225
+ || (Array.isArray(value.canonicalAttributions)
1226
+ && value.canonicalAttributions.every(isExperienceFeedbackAttribution)))
1227
+ && Array.isArray(value.attributions)
1228
+ && value.attributions.every(isExperienceFeedbackAttribution);
1229
+ }
1230
+ function isExperienceEpisodeArtifact(value) {
1231
+ return isObjectRecord(value)
1232
+ && isEnumValue(value.kind, [
1233
+ 'path',
1234
+ 'url',
1235
+ 'document',
1236
+ 'code',
1237
+ 'execution_window',
1238
+ 'unknown',
1239
+ ])
1240
+ && typeof value.label === 'string'
1241
+ && isOptionalString(value.pathOrUrl)
1242
+ && isEnumValue(value.artifactGoalMatch, ['passed', 'failed', 'unknown'])
1243
+ && isExperienceEvidenceRef(value.evidenceRef);
1244
+ }
1245
+ function isExperienceEpisodeOutcome(value) {
1246
+ return isObjectRecord(value)
1247
+ && isEnumValue(value.closure, ['closed', 'unresolved', 'abandoned', 'unknown'])
1248
+ && Array.isArray(value.artifacts)
1249
+ && value.artifacts.every(isExperienceEpisodeArtifact)
1250
+ && isExperienceReviewPriority(value.verdict)
1251
+ && isOptionalString(value.acceptanceCriteria);
1252
+ }
1253
+ function isExperienceEpisode(value) {
1254
+ return isObjectRecord(value)
1255
+ && typeof value.id === 'string'
1256
+ && isNonNegativeInteger(value.order)
1257
+ && typeof value.sessionId === 'string'
1258
+ && isOptionalString(value.primaryGoal)
1259
+ && Array.isArray(value.goalEvidenceRefs)
1260
+ && value.goalEvidenceRefs.every(isExperienceGoalEvidenceRef)
1261
+ && isTimestampRange(value.startTimestamp, value.endTimestamp)
1262
+ && (value.startRef === undefined || isExperienceEvidenceRef(value.startRef))
1263
+ && (value.endRef === undefined || isExperienceEvidenceRef(value.endRef))
1264
+ && isEnumValue(value.boundaryReason, [
1265
+ 'goal_shift',
1266
+ 'checkpoint_or_subagent',
1267
+ 'downstream_closed',
1268
+ 'session_end',
1269
+ ])
1270
+ && Array.isArray(value.skillSegments)
1271
+ && value.skillSegments.every(isExperienceSkillSegment)
1272
+ && Array.isArray(value.orchestrationEdges)
1273
+ && value.orchestrationEdges.every(isExperienceOrchestrationEdge)
1274
+ && Array.isArray(value.feedbackSignals)
1275
+ && value.feedbackSignals.every(isExperienceFeedbackSignal)
1276
+ && isExperienceEpisodeOutcome(value.outcome);
1277
+ }
1278
+ function isExperienceStoryContext(value) {
1279
+ return isObjectRecord(value)
1280
+ && typeof value.id === 'string'
1281
+ && typeof value.sessionGroupKey === 'string'
1282
+ && Array.isArray(value.goalSlices)
1283
+ && value.goalSlices.every(isExperienceSessionStoryGoalSlice)
1284
+ && Array.isArray(value.subagentDispatches)
1285
+ && value.subagentDispatches.every(isExperienceSessionStorySubagentDispatch)
1286
+ && Array.isArray(value.episodes)
1287
+ && value.episodes.every(isExperienceEpisode);
1288
+ }
1289
+ function isExperienceSessionStory(value, compact) {
1290
+ if (!isObjectRecord(value)
1291
+ || value.schemaVersion !== 1
1292
+ || (compact ? typeof value.contextRef !== 'string' : !isOptionalString(value.contextRef))
1293
+ || typeof value.summary !== 'string'
1294
+ || !isNonNegativeInteger(value.invocationCount)
1295
+ || !isNonNegativeInteger(value.goalSliceCount)
1296
+ || !isNonNegativeInteger(value.branchCount)
1297
+ || !isNonNegativeInteger(value.progressUpdateCount)
1298
+ || !isNonNegativeInteger(value.finalDeliverySignalCount)
1299
+ || !isStringArray(value.mainlineNodeIds)
1300
+ || !Array.isArray(value.skillLinks)
1301
+ || !value.skillLinks.every(isExperienceSessionStorySkillLink)
1302
+ || !isObjectRecord(value.graph)
1303
+ || !Array.isArray(value.graph.nodes)
1304
+ || !value.graph.nodes.every(isExperienceSessionStoryGraphNode)
1305
+ || !Array.isArray(value.graph.edges)
1306
+ || !value.graph.edges.every(isExperienceSessionStoryGraphEdge)
1307
+ || !Array.isArray(value.nodes)
1308
+ || !value.nodes.every(isExperienceSessionStoryNode)
1309
+ || !Array.isArray(value.answers)
1310
+ || !value.answers.every(isExperienceSessionStoryAnswer))
1311
+ return false;
1312
+ if (compact) {
1313
+ return value.goalSlices === undefined
1314
+ && value.subagentDispatches === undefined
1315
+ && value.episodes === undefined;
1316
+ }
1317
+ return Array.isArray(value.goalSlices)
1318
+ && value.goalSlices.every(isExperienceSessionStoryGoalSlice)
1319
+ && Array.isArray(value.subagentDispatches)
1320
+ && value.subagentDispatches.every(isExperienceSessionStorySubagentDispatch)
1321
+ && (value.episodes === undefined
1322
+ || (Array.isArray(value.episodes) && value.episodes.every(isExperienceEpisode)));
1323
+ }
1324
+ function isExperienceReviewerReportStep(value) {
1325
+ return isObjectRecord(value)
1326
+ && isNonNegativeInteger(value.order)
1327
+ && typeof value.label === 'string'
1328
+ && isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
1329
+ && typeof value.text === 'string'
1330
+ && isExperienceEvidenceRefArray(value.evidenceRefs);
1331
+ }
1332
+ function isExperienceReviewerReportFinding(value) {
1333
+ if (!isObjectRecord(value)
1334
+ || typeof value.id !== 'string'
1335
+ || typeof value.judgmentId !== 'string'
1336
+ || !isEnumValue(value.source, ['deterministic_rule', 'llm_soft', 'manual'])
1337
+ || !isEnumValue(value.level, ['attention', 'possible_false_positive', 'note'])
1338
+ || typeof value.title !== 'string'
1339
+ || typeof value.body !== 'string'
1340
+ || typeof value.ruleSource !== 'string'
1341
+ || typeof value.ruleVersion !== 'string'
1342
+ || !isExperienceEvidenceRefArray(value.evidenceRefs)
1343
+ || !isObjectRecord(value.reviewStateRef)
1344
+ || value.reviewStateRef.targetType !== 'reviewer_judgment'
1345
+ || typeof value.reviewStateRef.targetId !== 'string')
1346
+ return false;
1347
+ return [
1348
+ value.reviewStateRef.verdict,
1349
+ value.reviewStateRef.reason,
1350
+ value.reviewStateRef.note,
1351
+ ].every(isOptionalString)
1352
+ && isOptionalTimestamp(value.reviewStateRef.reviewedAt);
1353
+ }
1354
+ function isExperienceReviewerMetrics(value, requireSourceNeutralOutcomes) {
1355
+ if (!isObjectRecord(value))
1356
+ return false;
1357
+ const countKeys = [
1358
+ 'toolCallCount',
1359
+ 'toolFailureCount',
1360
+ 'userMessageCount',
1361
+ 'userFollowUpCount',
1362
+ 'assistantDeliverySignalCount',
1363
+ 'deliverableArtifactSignalCount',
1364
+ 'routerDownstreamCompleted',
1365
+ 'routerDownstreamFailed',
1366
+ 'assistantProgressUpdateCount',
1367
+ 'selfCorrectionCount',
1368
+ 'repeatedExecutionCount',
1369
+ 'finalDeliverySignalCount',
1370
+ 'traceEventCount',
1371
+ ];
1372
+ if (!countKeys.every((key) => isNonNegativeInteger(value[key]))
1373
+ || (requireSourceNeutralOutcomes
1374
+ && (!isNonNegativeInteger(value.toolCancelledCount)
1375
+ || !isNonNegativeInteger(value.toolUnknownCount)))
1376
+ || (value.toolCancelledCount !== undefined
1377
+ && !isNonNegativeInteger(value.toolCancelledCount))
1378
+ || (value.toolUnknownCount !== undefined
1379
+ && !isNonNegativeInteger(value.toolUnknownCount))
1380
+ || !isObjectRecord(value.tokenUsage))
1381
+ return false;
1382
+ const tokenKeys = [
1383
+ 'inputTokens',
1384
+ 'outputTokens',
1385
+ 'cacheReadTokens',
1386
+ 'cacheCreationTokens',
1387
+ ];
1388
+ const tokenUsage = value.tokenUsage;
1389
+ const observedInvocationCount = tokenUsage.observedInvocationCount;
1390
+ const invocationCount = tokenUsage.invocationCount;
1391
+ const coverage = tokenUsage.coverage;
1392
+ const hasCoverage = observedInvocationCount !== undefined
1393
+ || invocationCount !== undefined
1394
+ || coverage !== undefined;
1395
+ return tokenKeys.every((key) => isNonNegativeInteger(tokenUsage[key]))
1396
+ && tokenUsage.attribution === 'skill_segment'
1397
+ && (!hasCoverage
1398
+ || (isNonNegativeInteger(observedInvocationCount)
1399
+ && isNonNegativeInteger(invocationCount)
1400
+ && isRate(coverage)
1401
+ && observedInvocationCount <= invocationCount
1402
+ && Math.abs(coverage
1403
+ - (invocationCount > 0
1404
+ ? observedInvocationCount / invocationCount
1405
+ : 1)) <= 0.0001))
1406
+ && (!requireSourceNeutralOutcomes || hasCoverage)
1407
+ && value.toolFailureCount
1408
+ + (typeof value.toolCancelledCount === 'number' ? value.toolCancelledCount : 0)
1409
+ + (typeof value.toolUnknownCount === 'number' ? value.toolUnknownCount : 0)
1410
+ <= value.toolCallCount;
1411
+ }
1412
+ function isExperienceReviewerReport(value, compact) {
1413
+ if (!isObjectRecord(value)
1414
+ || value.schemaVersion !== 1
1415
+ || !isEnumValue(value.mode, ['deterministic_milestone_1', 'deterministic_session_story'])
1416
+ || !isTimestamp(value.generatedAt)
1417
+ || typeof value.title !== 'string'
1418
+ || typeof value.summary !== 'string'
1419
+ || !isObjectRecord(value.scope)
1420
+ || !isEnumValue(value.scope.kind, ['single_skill_single_goal', 'degraded_complex'])
1421
+ || !isStringArray(value.scope.reasonCodes)
1422
+ || !Array.isArray(value.chainSteps)
1423
+ || !value.chainSteps.every(isExperienceReviewerReportStep)
1424
+ || !Array.isArray(value.findings)
1425
+ || !value.findings.every(isExperienceReviewerReportFinding)
1426
+ || !isExperienceReviewerMetrics(value.oneLookMetrics, compact)
1427
+ || !isStringArray(value.authorSuggestions)
1428
+ || !isExperienceEvidenceRefArray(value.traceLinks))
1429
+ return false;
1430
+ if (compact) {
1431
+ return value.sessionStory === undefined && value.sessionStoryRef === 'session';
1432
+ }
1433
+ return isExperienceSessionStory(value.sessionStory, false)
1434
+ && (value.sessionStoryRef === undefined || value.sessionStoryRef === 'session');
1435
+ }
1436
+ /**
1437
+ * Converts a hydrated report into the wire format used by inbox JSON.
1438
+ * Timeline evidence is stored once per logical trace; consumers hydrate the
1439
+ * invocation/session views through normalizeObservationExperienceReport().
1440
+ */
1441
+ export function compactObservationExperienceReport(report) {
1442
+ const hydrated = hydrateExperienceTimelines(report);
1443
+ return {
1444
+ ...hydrated,
1445
+ invocations: hydrated.invocations.map((invocation) => {
1446
+ const { timeline, ...persisted } = invocation;
1447
+ return {
1448
+ ...persisted,
1449
+ timelineRef: invocation.timelineRef
1450
+ ?? timelineRefForSessionGroup(invocation.sessionGroupKey),
1451
+ timelineEventIds: invocation.timelineEventIds
1452
+ ?? timeline.map((event) => event.id),
1453
+ };
1454
+ }),
1455
+ sessions: hydrated.sessions.map((session) => {
1456
+ const { timelinePreview, sessionStory, reviewerReport } = session;
1457
+ const persisted = omitProperties(session, [
1458
+ 'timelinePreview',
1459
+ 'fullSessionTimeline',
1460
+ 'timelineTree',
1461
+ 'sessionStory',
1462
+ 'reviewerReport',
1463
+ ]);
1464
+ const contextRef = sessionStory?.contextRef;
1465
+ return {
1466
+ ...persisted,
1467
+ timelineRef: session.timelineRef
1468
+ ?? timelineRefForSessionGroup(session.sessionId),
1469
+ timelinePreviewEventIds: session.timelinePreviewEventIds
1470
+ ?? timelinePreview.map((event) => event.id),
1471
+ sessionStory: sessionStory && contextRef
1472
+ ? compactSessionStory(sessionStory, contextRef)
1473
+ : undefined,
1474
+ reviewerReport: reviewerReport && sessionStory && contextRef
1475
+ ? {
1476
+ ...omitSessionStory(reviewerReport),
1477
+ sessionStoryRef: 'session',
1478
+ }
1479
+ : undefined,
1480
+ };
1481
+ }),
1482
+ };
1483
+ }
1484
+ function hydrateExperienceTimelines(report) {
1485
+ const timelineById = new Map(report.traceTimelines.map((timeline) => [timeline.id, timeline]));
1486
+ const storyContextById = new Map(report.storyContexts.map((context) => [context.id, context]));
1487
+ const flattenedById = new Map();
1488
+ const eventByTimelineId = new Map();
1489
+ const flattenedTimeline = (ref) => {
1490
+ const cached = flattenedById.get(ref);
1491
+ if (cached)
1492
+ return cached;
1493
+ const timeline = timelineById.get(ref);
1494
+ if (!timeline)
1495
+ return [];
1496
+ const flattened = flattenTimelineTree(timeline.tree);
1497
+ flattenedById.set(ref, flattened);
1498
+ eventByTimelineId.set(ref, new Map(flattened.map((event) => [event.id, event])));
1499
+ return flattened;
1500
+ };
1501
+ const selectEvents = (ref, ids, fallback) => {
1502
+ if (!timelineById.has(ref))
1503
+ return fallback;
1504
+ if (!ids)
1505
+ return fallback.length > 0 ? fallback : flattenedTimeline(ref);
1506
+ const byId = eventByTimelineId.get(ref)
1507
+ ?? new Map(flattenedTimeline(ref).map((event) => [event.id, event]));
1508
+ return ids.flatMap((id) => {
1509
+ const event = byId.get(id);
1510
+ return event ? [event] : [];
1511
+ });
1512
+ };
1513
+ const invocations = report.invocations.map((invocation) => {
1514
+ const timelineRef = invocation.timelineRef ?? timelineRefForSessionGroup(invocation.sessionGroupKey);
1515
+ const timelineEventIds = invocation.timelineEventIds
1516
+ ?? invocation.timeline.map((event) => event.id);
1517
+ return {
1518
+ ...invocation,
1519
+ timelineRef,
1520
+ timelineEventIds,
1521
+ timeline: selectEvents(timelineRef, timelineEventIds, invocation.timeline),
1522
+ };
1523
+ });
1524
+ const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
1525
+ const sessions = report.sessions.map((session) => {
1526
+ const firstInvocation = session.invocationIds
1527
+ .map((id) => invocationById.get(id))
1528
+ .find((invocation) => Boolean(invocation));
1529
+ const timelineRef = session.timelineRef
1530
+ ?? firstInvocation?.timelineRef
1531
+ ?? timelineRefForSessionGroup(firstInvocation?.sessionGroupKey ?? session.sessionId);
1532
+ const storedTimeline = timelineById.get(timelineRef);
1533
+ const fullSessionTimeline = storedTimeline
1534
+ ? flattenedTimeline(timelineRef)
1535
+ : session.fullSessionTimeline;
1536
+ const timelinePreviewEventIds = session.timelinePreviewEventIds
1537
+ ?? session.timelinePreview.map((event) => event.id);
1538
+ const timelinePreview = timelinePreviewEventIds.length > 0
1539
+ ? selectEvents(timelineRef, timelinePreviewEventIds, session.timelinePreview)
1540
+ : fullSessionTimeline.slice(0, TIMELINE_PREVIEW_EVENT_LIMIT);
1541
+ const contextRef = session.sessionStory?.contextRef
1542
+ ?? storyContextRefForSessionGroup(firstInvocation?.sessionGroupKey ?? session.sessionId);
1543
+ const storyContext = storyContextById.get(contextRef);
1544
+ const sessionStory = session.sessionStory
1545
+ ? {
1546
+ ...session.sessionStory,
1547
+ contextRef,
1548
+ goalSlices: storyContext?.goalSlices ?? session.sessionStory.goalSlices,
1549
+ subagentDispatches: storyContext?.subagentDispatches
1550
+ ?? session.sessionStory.subagentDispatches,
1551
+ episodes: storyContext?.episodes ?? session.sessionStory.episodes ?? [],
1552
+ }
1553
+ : undefined;
1554
+ return {
1555
+ ...session,
1556
+ timelineRef,
1557
+ timelinePreviewEventIds: timelinePreview.map((event) => event.id),
1558
+ timelinePreview,
1559
+ fullSessionTimeline,
1560
+ timelineTree: storedTimeline?.tree ?? session.timelineTree,
1561
+ sessionStory,
1562
+ reviewerReport: session.reviewerReport && sessionStory
1563
+ ? {
1564
+ ...session.reviewerReport,
1565
+ sessionStory: session.reviewerReport.sessionStoryRef === 'session'
1566
+ ? sessionStory
1567
+ : session.reviewerReport.sessionStory,
1568
+ }
1569
+ : session.reviewerReport,
1570
+ };
1571
+ });
1572
+ return {
1573
+ ...report,
1574
+ schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
1575
+ invocations,
1576
+ sessions,
1577
+ };
1578
+ }
1579
+ function traceTimelinesFromSessions(sessions, invocations) {
1580
+ const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
1581
+ const timelines = new Map();
1582
+ for (const session of sessions) {
1583
+ const invocation = session.invocationIds
1584
+ .map((id) => invocationById.get(id))
1585
+ .find((value) => Boolean(value));
1586
+ const sessionGroupKey = invocation?.sessionGroupKey ?? session.sessionId;
1587
+ const id = session.timelineRef
1588
+ ?? invocation?.timelineRef
1589
+ ?? timelineRefForSessionGroup(sessionGroupKey);
1590
+ if (timelines.has(id))
1591
+ continue;
1592
+ const fallbackEvents = session.fullSessionTimeline.length > 0
1593
+ ? session.fullSessionTimeline
1594
+ : session.timelinePreview;
1595
+ const tree = session.timelineTree ?? {
1596
+ sessionId: session.sessionId,
1597
+ main: fallbackEvents,
1598
+ branches: [],
1599
+ };
1600
+ timelines.set(id, {
1601
+ id,
1602
+ sessionGroupKey,
1603
+ sessionId: session.sessionId,
1604
+ eventCount: flattenTimelineTree(tree).length,
1605
+ tree,
1606
+ });
1607
+ }
1608
+ return Array.from(timelines.values());
1609
+ }
1610
+ function validateExperienceReferences(report, strict) {
1611
+ if (!isObjectRecord(report.meta)
1612
+ || report.meta.sessionCount !== report.sessions.length
1613
+ || report.meta.skillCount !== report.skills.length
1614
+ || report.meta.invocationCount !== report.invocations.length
1615
+ || report.meta.goalSliceCount !== report.goalSlices.length)
1616
+ return false;
1617
+ const timelineEventsByRef = new Map();
1618
+ const timelineByRef = new Map();
1619
+ const timelineSessionGroupKeys = new Set();
1620
+ for (const timeline of report.traceTimelines) {
1621
+ if (timelineEventsByRef.has(timeline.id)
1622
+ || timelineSessionGroupKeys.has(timeline.sessionGroupKey))
1623
+ return false;
1624
+ if (timeline.tree.sessionId !== timeline.sessionId
1625
+ || (strict && !traceTimelineStructureIsConsistent(timeline)))
1626
+ return false;
1627
+ const rawEvents = [
1628
+ ...timeline.tree.main,
1629
+ ...timeline.tree.branches.flatMap((branch) => branch.events),
1630
+ ];
1631
+ if (strict
1632
+ && (rawEvents.some((event) => typeof event.traceId !== 'string')
1633
+ || rawEvents.some((event) => (event.kind === 'tool_use' || event.kind === 'tool_result')
1634
+ && typeof event.callInstanceId !== 'string')
1635
+ || timeline.tree.branches.some((branch) => typeof branch.traceId !== 'string'
1636
+ || (branch.attachTo !== undefined && typeof branch.attachTo.traceId !== 'string'))))
1637
+ return false;
1638
+ if (new Set(rawEvents.map((event) => event.id)).size !== rawEvents.length)
1639
+ return false;
1640
+ const events = flattenTimelineTree(timeline.tree);
1641
+ if (timeline.eventCount !== events.length)
1642
+ return false;
1643
+ timelineEventsByRef.set(timeline.id, new Set(events.map((event) => event.id)));
1644
+ timelineByRef.set(timeline.id, timeline);
1645
+ timelineSessionGroupKeys.add(timeline.sessionGroupKey);
1646
+ }
1647
+ const goalSliceIds = new Set();
1648
+ const goalSliceById = new Map();
1649
+ for (const goalSlice of report.goalSlices) {
1650
+ if (!isExperienceGoalSlice(goalSlice)
1651
+ || (strict
1652
+ && (typeof goalSlice.traceId !== 'string'
1653
+ || typeof goalSlice.timestampObserved !== 'boolean'))
1654
+ || goalSliceIds.has(goalSlice.id))
1655
+ return false;
1656
+ goalSliceIds.add(goalSlice.id);
1657
+ goalSliceById.set(goalSlice.id, goalSlice);
1658
+ }
1659
+ const invocationById = new Map();
1660
+ const referencedGoalSliceIds = new Set();
1661
+ for (const invocation of report.invocations) {
1662
+ if (invocationById.has(invocation.id))
1663
+ return false;
1664
+ invocationById.set(invocation.id, invocation);
1665
+ if (strict
1666
+ && (countRecordTotal(invocation.toolCounts) !== invocation.metrics.numToolCalls
1667
+ || !toolOutcomeCountsMatch(invocation.metrics, invocation.indicators)))
1668
+ return false;
1669
+ if (!goalSliceIds.has(invocation.goalSliceId))
1670
+ return false;
1671
+ const goalSlice = goalSliceById.get(invocation.goalSliceId);
1672
+ if (goalSlice
1673
+ && (goalSlice.skillName !== invocation.skillName
1674
+ || goalSlice.sessionId !== invocation.sessionId
1675
+ || goalSlice.sourceTrace !== invocation.sourceTrace
1676
+ || goalSlice.startTimestamp !== invocation.startTimestamp
1677
+ || goalSlice.endTimestamp !== invocation.endTimestamp
1678
+ || (strict
1679
+ && (goalSlice.traceId !== invocation.traceId
1680
+ || goalSlice.timestampObserved !== invocation.timestampObserved))))
1681
+ return false;
1682
+ referencedGoalSliceIds.add(invocation.goalSliceId);
1683
+ if (strict && (!invocation.timelineRef || !invocation.timelineEventIds))
1684
+ return false;
1685
+ if (!invocation.timelineRef) {
1686
+ if (invocation.timeline.length === 0 && (invocation.timelineEventIds?.length ?? 0) > 0)
1687
+ return false;
1688
+ continue;
1689
+ }
1690
+ const eventIds = timelineEventsByRef.get(invocation.timelineRef);
1691
+ if (!eventIds)
1692
+ return false;
1693
+ if (timelineByRef.get(invocation.timelineRef)?.sessionGroupKey !== invocation.sessionGroupKey)
1694
+ return false;
1695
+ if (invocation.timelineEventIds
1696
+ && new Set(invocation.timelineEventIds).size !== invocation.timelineEventIds.length)
1697
+ return false;
1698
+ if (invocation.timelineEventIds?.some((id) => !eventIds.has(id)))
1699
+ return false;
1700
+ }
1701
+ if (referencedGoalSliceIds.size !== goalSliceIds.size)
1702
+ return false;
1703
+ const sessionIds = new Set();
1704
+ const invocationReferenceCounts = new Map(report.invocations.map((invocation) => [invocation.id, 0]));
1705
+ const referencedTimelineIds = new Set();
1706
+ const referencedStoryContextIds = new Set();
1707
+ for (const session of report.sessions) {
1708
+ if (sessionIds.has(session.id))
1709
+ return false;
1710
+ sessionIds.add(session.id);
1711
+ if (session.invocationIds.length === 0
1712
+ || new Set(session.invocationIds).size !== session.invocationIds.length
1713
+ || session.invocationIds.some((id) => !invocationById.has(id)))
1714
+ return false;
1715
+ if (session.goalSliceIds.some((id) => !goalSliceIds.has(id)))
1716
+ return false;
1717
+ const sessionInvocations = session.invocationIds
1718
+ .map((id) => invocationById.get(id))
1719
+ .filter((value) => Boolean(value));
1720
+ const sessionGroupKeys = new Set(sessionInvocations.map((invocation) => invocation.sessionGroupKey));
1721
+ const invocationGoalSliceIds = new Set(sessionInvocations.map((invocation) => invocation.goalSliceId));
1722
+ if (sessionInvocations.some((invocation) => invocation.skillName !== session.skillName)
1723
+ || sessionGroupKeys.size !== 1
1724
+ || session.goalSliceIds.length !== invocationGoalSliceIds.size
1725
+ || session.goalSliceIds.some((id) => !invocationGoalSliceIds.has(id)))
1726
+ return false;
1727
+ for (const invocation of sessionInvocations) {
1728
+ invocationReferenceCounts.set(invocation.id, (invocationReferenceCounts.get(invocation.id) ?? 0) + 1);
1729
+ }
1730
+ if (strict) {
1731
+ const timestampedInvocations = sessionInvocations.filter(invocationTimestampObserved);
1732
+ const expectedStartTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? sessionInvocations[0]?.startTimestamp;
1733
+ const expectedEndTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? sessionInvocations[0]?.endTimestamp;
1734
+ if (sessionInvocations.some((invocation) => invocation.timelineRef !== session.timelineRef))
1735
+ return false;
1736
+ const invocationIndicators = sumIndicators(sessionInvocations.map((invocation) => invocation.indicators));
1737
+ const expectedIndicators = enrichRouterDownstreamIndicators({
1738
+ ...session,
1739
+ indicators: invocationIndicators,
1740
+ });
1741
+ const expectedReviewPriorityScore = scoreForIndicators(expectedIndicators);
1742
+ const expectedReviewPriority = session.reviewerReport
1743
+ ? priorityForReviewerFindings({
1744
+ ...session,
1745
+ indicators: expectedIndicators,
1746
+ reviewPriorityScore: expectedReviewPriorityScore,
1747
+ }, session.reviewerReport.findings)
1748
+ : priorityForScore(expectedReviewPriorityScore);
1749
+ if (!indicatorRecordsEqual(session.indicators, expectedIndicators)
1750
+ || session.reviewPriorityScore !== expectedReviewPriorityScore
1751
+ || session.reviewPriority !== expectedReviewPriority
1752
+ || !sameStringArray(session.reviewBasisCodes, basisCodesForIndicators(expectedIndicators))
1753
+ || session.startTimestamp !== expectedStartTimestamp
1754
+ || session.endTimestamp !== expectedEndTimestamp
1755
+ || session.timestampedInvocationCount !== timestampedInvocations.length
1756
+ || session.timestampCoverage !== timestampedInvocations.length / sessionInvocations.length)
1757
+ return false;
1758
+ const sessionGroupKey = sessionInvocations[0]?.sessionGroupKey;
1759
+ if (sessionGroupKey
1760
+ && timelineByRef.get(session.timelineRef ?? '')?.sessionGroupKey !== sessionGroupKey)
1761
+ return false;
1762
+ }
1763
+ if (strict && (!session.timelineRef || !session.timelinePreviewEventIds))
1764
+ return false;
1765
+ if (session.timelineRef) {
1766
+ referencedTimelineIds.add(session.timelineRef);
1767
+ const eventIds = timelineEventsByRef.get(session.timelineRef);
1768
+ const timeline = timelineByRef.get(session.timelineRef);
1769
+ if (!eventIds)
1770
+ return false;
1771
+ if (session.timelinePreviewEventIds
1772
+ && new Set(session.timelinePreviewEventIds).size !== session.timelinePreviewEventIds.length)
1773
+ return false;
1774
+ if (session.timelinePreviewEventIds?.some((id) => !eventIds.has(id)))
1775
+ return false;
1776
+ if (strict
1777
+ && (timeline?.sessionId !== session.sessionId
1778
+ || !timelineScopeMatches(session, sessionInvocations, timeline)))
1779
+ return false;
1780
+ }
1781
+ else if (session.fullSessionTimeline.length === 0
1782
+ && session.timelinePreview.length === 0
1783
+ && (session.timelinePreviewEventIds?.length ?? 0) > 0) {
1784
+ return false;
1785
+ }
1786
+ if (session.sessionStory?.contextRef) {
1787
+ referencedStoryContextIds.add(session.sessionStory.contextRef);
1788
+ }
1789
+ if (strict
1790
+ && session.reviewerReport
1791
+ && !reviewerMetricsMatch(session.reviewerReport, session, sessionInvocations, timelineByRef.get(session.timelineRef ?? '')))
1792
+ return false;
1793
+ }
1794
+ if (Array.from(invocationReferenceCounts.values()).some((count) => count !== 1)
1795
+ || (strict && referencedTimelineIds.size !== timelineEventsByRef.size))
1796
+ return false;
1797
+ const storyContextIds = new Set();
1798
+ const storyContextById = new Map();
1799
+ const storyContextSessionGroupKeys = new Set();
1800
+ for (const context of report.storyContexts) {
1801
+ if (storyContextIds.has(context.id)
1802
+ || storyContextSessionGroupKeys.has(context.sessionGroupKey))
1803
+ return false;
1804
+ storyContextIds.add(context.id);
1805
+ storyContextById.set(context.id, context);
1806
+ storyContextSessionGroupKeys.add(context.sessionGroupKey);
1807
+ }
1808
+ const skillNames = new Set();
1809
+ for (const skill of report.skills) {
1810
+ if (!isExperienceSkillSummary(skill, strict)
1811
+ || skillNames.has(skill.skillName))
1812
+ return false;
1813
+ if (strict) {
1814
+ const skillInvocations = report.invocations.filter((invocation) => invocation.skillName === skill.skillName);
1815
+ const skillSessions = report.sessions.filter((session) => session.skillName === skill.skillName);
1816
+ const timestampedInvocations = skillInvocations.filter(invocationTimestampObserved);
1817
+ const expectedFirstSeen = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? skillSessions[0]?.startTimestamp;
1818
+ const expectedLastSeen = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? skillSessions[0]?.endTimestamp;
1819
+ if (skill.invocationCount !== skillInvocations.length
1820
+ || skill.sessionCount !== skillSessions.length
1821
+ || !indicatorRecordsEqual(skill.indicators, sumIndicators(skillSessions.map((session) => session.indicators)))
1822
+ || !countRecordsEqual(skill.toolCounts, sumRecordCounts(skillInvocations.map((invocation) => invocation.toolCounts)))
1823
+ || skill.firstSeen !== expectedFirstSeen
1824
+ || skill.lastSeen !== expectedLastSeen
1825
+ || skill.timestampedInvocationCount !== timestampedInvocations.length
1826
+ || skill.timestampCoverage !== (skillInvocations.length > 0
1827
+ ? timestampedInvocations.length / skillInvocations.length
1828
+ : 0)
1829
+ || skill.reviewFirstSessionCount !== skillSessions.filter((session) => session.reviewPriority === 'review_first').length
1830
+ || skill.sampleReviewSessionCount !== skillSessions.filter((session) => session.reviewPriority === 'sample_review').length)
1831
+ return false;
1832
+ }
1833
+ skillNames.add(skill.skillName);
1834
+ }
1835
+ if (report.sessions.some((session) => !skillNames.has(session.skillName))
1836
+ || report.invocations.some((invocation) => !skillNames.has(invocation.skillName))
1837
+ || (strict && referencedStoryContextIds.size !== storyContextIds.size))
1838
+ return false;
1839
+ return report.sessions.every((session) => {
1840
+ const contextRef = session.sessionStory?.contextRef;
1841
+ if (strict && session.sessionStory && !contextRef)
1842
+ return false;
1843
+ if (contextRef && !storyContextIds.has(contextRef))
1844
+ return false;
1845
+ if (strict && contextRef) {
1846
+ const firstInvocation = session.invocationIds
1847
+ .map((id) => invocationById.get(id))
1848
+ .find((value) => Boolean(value));
1849
+ const storyInvocations = firstInvocation
1850
+ ? report.invocations.filter((invocation) => invocation.sessionGroupKey === firstInvocation.sessionGroupKey)
1851
+ : [];
1852
+ const context = storyContextById.get(contextRef);
1853
+ const timeline = timelineByRef.get(session.timelineRef ?? '');
1854
+ if (firstInvocation
1855
+ && context?.sessionGroupKey !== firstInvocation.sessionGroupKey)
1856
+ return false;
1857
+ if (session.sessionStory
1858
+ && context
1859
+ && timeline
1860
+ && !sessionStoryStructureIsConsistent(session.sessionStory, context, storyInvocations, timeline))
1861
+ return false;
1862
+ }
1863
+ if (strict
1864
+ && session.reviewerReport
1865
+ && session.reviewerReport.sessionStoryRef !== 'session'
1866
+ && !isObjectRecord(session.reviewerReport.sessionStory))
1867
+ return false;
1868
+ return session.reviewerReport?.sessionStoryRef !== 'session' || Boolean(session.sessionStory);
1869
+ });
1870
+ }
1871
+ function sessionStoryStructureIsConsistent(story, context, invocations, timeline) {
1872
+ const timelineEvents = flattenTimelineTree(timeline.tree);
1873
+ const expectedGoalSliceIds = new Set(invocations.map((invocation) => invocation.goalSliceId));
1874
+ const contextGoalSliceIds = context.goalSlices.map((goalSlice) => goalSlice.id);
1875
+ if (story.invocationCount !== invocations.length
1876
+ || story.goalSliceCount !== expectedGoalSliceIds.size
1877
+ || contextGoalSliceIds.length !== expectedGoalSliceIds.size
1878
+ || new Set(contextGoalSliceIds).size !== contextGoalSliceIds.length
1879
+ || contextGoalSliceIds.some((id) => !expectedGoalSliceIds.has(id))
1880
+ || story.branchCount !== timeline.tree.branches.length
1881
+ || story.progressUpdateCount !== timelineEvents.filter(isAssistantProgressUpdateEvent).length
1882
+ || story.finalDeliverySignalCount !== timelineEvents.filter(isAssistantDeliveryEvent).length)
1883
+ return false;
1884
+ if (!storyContextEpisodesAreConsistent(context, invocations, timeline.sessionId))
1885
+ return false;
1886
+ const branchById = new Map(timeline.tree.branches.map((branch) => [branch.id, branch]));
1887
+ if (context.subagentDispatches.length !== branchById.size
1888
+ || new Set(context.subagentDispatches.map((dispatch) => dispatch.id)).size
1889
+ !== context.subagentDispatches.length
1890
+ || new Set(context.subagentDispatches.map((dispatch) => dispatch.branchId)).size
1891
+ !== context.subagentDispatches.length
1892
+ || context.subagentDispatches.some((dispatch) => {
1893
+ const branch = branchById.get(dispatch.branchId);
1894
+ return !branch
1895
+ || dispatch.childSessionId !== branch.sessionId
1896
+ || dispatch.traceId !== (branch.traceId ?? branch.sourceTrace)
1897
+ || dispatch.sourceTrace !== branch.sourceTrace
1898
+ || dispatch.eventCount !== branch.events.length;
1899
+ }))
1900
+ return false;
1901
+ const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
1902
+ const invocationReferenceCounts = new Map(invocations.map((invocation) => [invocation.id, 0]));
1903
+ const skillLinkIds = new Set();
1904
+ for (const link of story.skillLinks) {
1905
+ if (skillLinkIds.has(link.id)
1906
+ || new Set(link.invocationIds).size !== link.invocationIds.length
1907
+ || link.invocationIds.some((id) => !invocationById.has(id)))
1908
+ return false;
1909
+ skillLinkIds.add(link.id);
1910
+ const linkedInvocations = link.invocationIds
1911
+ .map((id) => invocationById.get(id))
1912
+ .filter((value) => Boolean(value));
1913
+ const linkedGoalSliceIds = new Set(linkedInvocations.map((invocation) => invocation.goalSliceId));
1914
+ if (linkedInvocations.some((invocation) => invocation.skillName !== link.skillName)
1915
+ || link.goalSliceIds.length !== linkedGoalSliceIds.size
1916
+ || link.goalSliceIds.some((id) => !linkedGoalSliceIds.has(id)))
1917
+ return false;
1918
+ for (const invocation of linkedInvocations) {
1919
+ invocationReferenceCounts.set(invocation.id, (invocationReferenceCounts.get(invocation.id) ?? 0) + 1);
1920
+ }
1921
+ }
1922
+ if ([...invocationReferenceCounts.values()].some((count) => count !== 1))
1923
+ return false;
1924
+ const nodeIds = story.nodes.map((node) => node.id);
1925
+ const nodeIdSet = new Set(nodeIds);
1926
+ const graphNodeIds = story.graph.nodes.map((node) => node.id);
1927
+ const expectedGraphNodeIds = new Set([...nodeIds, ...skillLinkIds]);
1928
+ if (nodeIdSet.size !== nodeIds.length
1929
+ || new Set(story.mainlineNodeIds).size !== story.mainlineNodeIds.length
1930
+ || story.mainlineNodeIds.some((id) => !nodeIdSet.has(id))
1931
+ || new Set(graphNodeIds).size !== graphNodeIds.length
1932
+ || graphNodeIds.length !== expectedGraphNodeIds.size
1933
+ || graphNodeIds.some((id) => !expectedGraphNodeIds.has(id))
1934
+ || story.graph.nodes.some((node) => node.detailNodeId !== undefined && !nodeIdSet.has(node.detailNodeId))
1935
+ || story.graph.edges.some((edge) => !expectedGraphNodeIds.has(edge.fromId) || !expectedGraphNodeIds.has(edge.toId)))
1936
+ return false;
1937
+ return true;
1938
+ }
1939
+ function timelineScopeMatches(session, invocations, timeline) {
1940
+ if (!timeline)
1941
+ return false;
1942
+ const events = flattenTimelineTree(timeline.tree);
1943
+ const eventById = new Map(events.map((event) => [event.id, event]));
1944
+ const invocationEventIds = new Set(invocations.flatMap((invocation) => invocation.timelineEventIds ?? []));
1945
+ const previewEventIds = session.timelinePreviewEventIds ?? [];
1946
+ if (previewEventIds.some((id) => !invocationEventIds.has(id)))
1947
+ return false;
1948
+ const invocationEvents = [...invocationEventIds]
1949
+ .map((id) => eventById.get(id))
1950
+ .filter((event) => Boolean(event));
1951
+ if (invocationEvents.length !== invocationEventIds.size)
1952
+ return false;
1953
+ const previewEvents = previewEventIds
1954
+ .map((id) => eventById.get(id))
1955
+ .filter((event) => Boolean(event));
1956
+ const eventPositionById = new Map(events.map((event, index) => [event.id, index]));
1957
+ const previewPositions = previewEventIds
1958
+ .map((id) => eventPositionById.get(id))
1959
+ .filter((index) => typeof index === 'number');
1960
+ const omittedBeforeCount = previewPositions.length > 0 ? Math.min(...previewPositions) : 0;
1961
+ const omittedAfterCount = previewPositions.length > 0
1962
+ ? events.length - 1 - Math.max(...previewPositions)
1963
+ : 0;
1964
+ const scope = session.timelineScope;
1965
+ return scope.segmentEventCount === invocationEventIds.size
1966
+ && scope.previewEventCount === previewEventIds.length
1967
+ && scope.fullSessionEventCount === events.length
1968
+ && traceRecordRangesEqual(scope.segmentRecordRanges, traceRecordRanges(invocationEvents))
1969
+ && traceRecordRangesEqual(scope.previewRecordRanges, traceRecordRanges(previewEvents))
1970
+ && traceRecordRangesEqual(scope.sessionRecordRanges, traceRecordRanges(events))
1971
+ && scope.omittedBeforeCount === omittedBeforeCount
1972
+ && scope.omittedAfterCount === omittedAfterCount
1973
+ && scope.truncated === (invocationEventIds.size > previewEventIds.length
1974
+ || omittedBeforeCount > 0
1975
+ || omittedAfterCount > 0);
1976
+ }
1977
+ function traceRecordRangesEqual(left, right) {
1978
+ return left.length === right.length
1979
+ && left.every((range, index) => range.traceId === right[index].traceId
1980
+ && range.sourceTrace === right[index].sourceTrace
1981
+ && range.startRecordIndex === right[index].startRecordIndex
1982
+ && range.endRecordIndex === right[index].endRecordIndex
1983
+ && range.eventCount === right[index].eventCount);
1984
+ }
1985
+ function storyContextEpisodesAreConsistent(context, invocations, sessionId) {
1986
+ const goalSliceIds = new Set(context.goalSlices.map((goalSlice) => goalSlice.id));
1987
+ const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
1988
+ const episodeIds = new Set();
1989
+ for (const episode of context.episodes) {
1990
+ if (episodeIds.has(episode.id) || episode.sessionId !== sessionId)
1991
+ return false;
1992
+ episodeIds.add(episode.id);
1993
+ const skillSegmentById = new Map();
1994
+ for (const segment of episode.skillSegments) {
1995
+ if (skillSegmentById.has(segment.id)
1996
+ || new Set(segment.skillInvocationIds).size !== segment.skillInvocationIds.length
1997
+ || segment.skillInvocationIds.some((id) => invocationById.get(id)?.skillName !== segment.skillName)
1998
+ || Date.parse(segment.startTimestamp) < Date.parse(episode.startTimestamp)
1999
+ || Date.parse(segment.endTimestamp) > Date.parse(episode.endTimestamp))
2000
+ return false;
2001
+ skillSegmentById.set(segment.id, segment);
2002
+ }
2003
+ const edgeIds = new Set();
2004
+ for (const edge of episode.orchestrationEdges) {
2005
+ const parent = edge.parentSkillSegmentId
2006
+ ? skillSegmentById.get(edge.parentSkillSegmentId)
2007
+ : undefined;
2008
+ const executor = edge.executorSkillSegmentId
2009
+ ? skillSegmentById.get(edge.executorSkillSegmentId)
2010
+ : undefined;
2011
+ if (edgeIds.has(edge.id)
2012
+ || edge.episodeId !== episode.id
2013
+ || (edge.parentSkillSegmentId !== undefined && !parent)
2014
+ || (edge.executorSkillSegmentId !== undefined && !executor)
2015
+ || (edge.edgeKind === 'internal_skill'
2016
+ && (!parent || !executor || parent.id === executor.id)))
2017
+ return false;
2018
+ edgeIds.add(edge.id);
2019
+ }
2020
+ const signalIds = new Set();
2021
+ for (const signal of episode.feedbackSignals) {
2022
+ if (signalIds.has(signal.id))
2023
+ return false;
2024
+ signalIds.add(signal.id);
2025
+ const attributionGroups = [
2026
+ signal.attributions,
2027
+ signal.canonicalAttributions ?? [],
2028
+ ];
2029
+ for (const attribution of attributionGroups.flat()) {
2030
+ if (!attribution.skillSegmentId)
2031
+ continue;
2032
+ const segment = skillSegmentById.get(attribution.skillSegmentId);
2033
+ if (!segment || (attribution.skillName && attribution.skillName !== segment.skillName)) {
2034
+ return false;
2035
+ }
2036
+ }
2037
+ }
2038
+ if (episode.goalEvidenceRefs.some((ref) => ref.kind === 'goal_slice'
2039
+ ? !ref.goalSliceId || !goalSliceIds.has(ref.goalSliceId)
2040
+ : ref.goalSliceId !== undefined && !goalSliceIds.has(ref.goalSliceId)))
2041
+ return false;
2042
+ }
2043
+ return true;
2044
+ }
2045
+ function traceTimelineStructureIsConsistent(timeline) {
2046
+ const branches = timeline.tree.branches;
2047
+ if (new Set(branches.map((branch) => branch.id)).size !== branches.length)
2048
+ return false;
2049
+ if (timeline.tree.main.some((event) => event.sessionId !== timeline.sessionId))
2050
+ return false;
2051
+ const mainTraceIds = new Set(timeline.tree.main
2052
+ .map((event) => event.traceId)
2053
+ .filter((value) => Boolean(value)));
2054
+ const mainSourceTraces = new Set(timeline.tree.main.map((event) => event.sourceTrace));
2055
+ if (mainTraceIds.size > 1 || mainSourceTraces.size > 1)
2056
+ return false;
2057
+ for (const branch of branches) {
2058
+ if (branch.events.some((event) => event.sessionId !== timeline.sessionId
2059
+ || (branch.traceId !== undefined && event.traceId !== branch.traceId)
2060
+ || event.sourceTrace !== branch.sourceTrace
2061
+ || event.traceRole !== branch.traceRole))
2062
+ return false;
2063
+ if (branch.attachTo
2064
+ && !timeline.tree.main.some((event) => (branch.attachTo?.traceId !== undefined
2065
+ ? event.traceId === branch.attachTo.traceId
2066
+ : event.sourceTrace === branch.attachTo?.sourceTrace)
2067
+ && event.sourceTrace === branch.attachTo?.sourceTrace
2068
+ && (branch.attachTo.messageIndex === undefined
2069
+ || event.messageIndex === branch.attachTo.messageIndex)
2070
+ && (branch.attachTo.callInstanceId === undefined
2071
+ || event.callInstanceId === branch.attachTo.callInstanceId)
2072
+ && (branch.attachTo.toolUseId === undefined
2073
+ || event.toolUseId === branch.attachTo.toolUseId)
2074
+ && (branch.attachTo.label === undefined
2075
+ || event.label === branch.attachTo.label
2076
+ || event.toolName === branch.attachTo.label)))
2077
+ return false;
2078
+ }
2079
+ return true;
2080
+ }
2081
+ function toolOutcomeCountsMatch(left, right) {
2082
+ const normalized = 'numToolCalls' in left
2083
+ ? {
2084
+ toolCallCount: left.numToolCalls,
2085
+ toolFailureCount: left.numToolFailures,
2086
+ toolCancelledCount: left.numToolCancelled,
2087
+ toolUnknownCount: left.numToolUnknown,
2088
+ }
2089
+ : left;
2090
+ return normalized.toolCallCount === right.toolCallCount
2091
+ && normalized.toolFailureCount === right.toolFailureCount
2092
+ && (normalized.toolCancelledCount ?? 0) === (right.toolCancelledCount ?? 0)
2093
+ && (normalized.toolUnknownCount ?? 0) === (right.toolUnknownCount ?? 0);
2094
+ }
2095
+ function indicatorRecordsEqual(left, right) {
2096
+ const keys = [
2097
+ ...EXPERIENCE_INDICATOR_KEYS,
2098
+ 'toolCancelledCount',
2099
+ 'toolUnknownCount',
2100
+ ];
2101
+ return keys.every((key) => (left[key] ?? 0) === (right[key] ?? 0));
2102
+ }
2103
+ function reviewerMetricsMatch(reviewer, session, invocations, timeline) {
2104
+ const metrics = reviewer.oneLookMetrics;
2105
+ const expectedUsage = sumTokenUsage(invocations);
2106
+ const timelineEvents = timeline ? flattenTimelineTree(timeline.tree) : [];
2107
+ const hydratedSession = {
2108
+ ...session,
2109
+ fullSessionTimeline: timelineEvents,
2110
+ timelineTree: timeline?.tree,
2111
+ };
2112
+ return toolOutcomeCountsMatch(metrics, session.indicators)
2113
+ && metrics.userMessageCount === session.indicators.userMessageCount
2114
+ && metrics.assistantDeliverySignalCount === session.indicators.assistantDeliverySignalCount
2115
+ && metrics.deliverableArtifactSignalCount === session.indicators.deliverableArtifactSignalCount
2116
+ && metrics.routerDownstreamCompleted === session.indicators.routerDownstreamCompleted
2117
+ && metrics.routerDownstreamFailed === session.indicators.routerDownstreamFailed
2118
+ && metrics.assistantProgressUpdateCount === assistantProgressUpdateEvents(hydratedSession).length
2119
+ && metrics.selfCorrectionCount === session.indicators.selfCorrectionCount
2120
+ && metrics.repeatedExecutionCount === session.indicators.repeatedExecutionCount
2121
+ && metrics.finalDeliverySignalCount === assistantFinalDeliveryEvents(hydratedSession).length
2122
+ && metrics.traceEventCount === timelineEvents.length
2123
+ && metrics.tokenUsage.inputTokens === expectedUsage.inputTokens
2124
+ && metrics.tokenUsage.outputTokens === expectedUsage.outputTokens
2125
+ && metrics.tokenUsage.cacheReadTokens === expectedUsage.cacheReadTokens
2126
+ && metrics.tokenUsage.cacheCreationTokens === expectedUsage.cacheCreationTokens
2127
+ && metrics.tokenUsage.observedInvocationCount === expectedUsage.observedInvocationCount
2128
+ && metrics.tokenUsage.invocationCount === expectedUsage.invocationCount
2129
+ && metrics.tokenUsage.coverage === expectedUsage.coverage;
2130
+ }
2131
+ function countRecordTotal(value) {
2132
+ return sumSafeCounts(...Object.values(value));
2133
+ }
2134
+ function countRecordsEqual(left, right) {
2135
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
2136
+ return [...keys].every((key) => (left[key] ?? 0) === (right[key] ?? 0));
2137
+ }
2138
+ function sameStringArray(left, right) {
2139
+ return left.length === right.length && left.every((value, index) => value === right[index]);
2140
+ }
2141
+ function storyContextsFromSessions(sessions, invocations) {
2142
+ const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
2143
+ const contexts = new Map();
2144
+ for (const session of sessions) {
2145
+ const story = session.sessionStory;
2146
+ if (!story)
2147
+ continue;
2148
+ const invocation = session.invocationIds
2149
+ .map((id) => invocationById.get(id))
2150
+ .find((value) => Boolean(value));
2151
+ const sessionGroupKey = invocation?.sessionGroupKey ?? session.sessionId;
2152
+ const id = story.contextRef ?? storyContextRefForSessionGroup(sessionGroupKey);
2153
+ if (contexts.has(id))
2154
+ continue;
2155
+ contexts.set(id, {
2156
+ id,
2157
+ sessionGroupKey,
2158
+ goalSlices: story.goalSlices,
2159
+ subagentDispatches: story.subagentDispatches,
2160
+ episodes: story.episodes ?? [],
2161
+ });
2162
+ }
2163
+ return Array.from(contexts.values());
2164
+ }
2165
+ function timelineRefForSessionGroup(sessionGroupKey) {
2166
+ return hashParts('trace-timeline', sessionGroupKey);
2167
+ }
2168
+ function storyContextRefForSessionGroup(sessionGroupKey) {
2169
+ return hashParts('story-context', sessionGroupKey);
2170
+ }
2171
+ function flattenTimelineTree(tree) {
2172
+ return uniqueTimelineEvents([
2173
+ ...tree.main,
2174
+ ...tree.branches.flatMap((branch) => branch.events),
2175
+ ]).sort(compareTimelineEvents);
2176
+ }
2177
+ function compactSessionStory(story, contextRef) {
2178
+ const persisted = omitProperties(story, [
2179
+ 'goalSlices',
2180
+ 'subagentDispatches',
2181
+ 'episodes',
2182
+ ]);
2183
+ return { ...persisted, contextRef };
2184
+ }
2185
+ function omitSessionStory(report) {
2186
+ return omitProperties(report, ['sessionStory', 'sessionStoryRef']);
2187
+ }
2188
+ function omitProperties(value, keys) {
2189
+ const copy = { ...value };
2190
+ for (const key of keys)
2191
+ delete copy[key];
2192
+ return copy;
179
2193
  }
180
2194
  function relatedObservationItems(segment, items) {
181
2195
  return items.filter((item) => item.skillName === segment.skillName
2196
+ && (item.traceId && segment.traceId
2197
+ ? item.traceId === segment.traceId
2198
+ : true)
2199
+ // main/subagent 可以共享逻辑 sessionId,且调用同一个 skill。新数据都有
2200
+ // sourceTrace,必须严格按物理 trace 归因;空字符串仅用于兼容旧聚合数据。
2201
+ && (!item.sourceTrace || !segment.sourceTrace || item.sourceTrace === segment.sourceTrace)
182
2202
  && (item.sessionId === segment.sessionId || item.recentSessionIds.includes(segment.sessionId))
183
2203
  && timestampsOverlap(item.firstSeen, item.lastSeen, segment.startTimestamp, segment.endTimestamp));
184
2204
  }
2205
+ function metricTimelineForSegment(timeline, segment) {
2206
+ const callInstanceIds = new Set(segment.toolCalls.flatMap((call) => call.callInstanceId ? [call.callInstanceId] : []));
2207
+ const legacyToolUseIds = new Set(segment.toolCalls.flatMap((call) => !call.callInstanceId && call.toolUseId ? [call.toolUseId] : []));
2208
+ return timeline.filter((event) => (event.kind !== 'tool_use' && event.kind !== 'tool_result')
2209
+ || (event.callInstanceId
2210
+ ? callInstanceIds.has(event.callInstanceId)
2211
+ : Boolean(event.toolUseId) && legacyToolUseIds.has(event.toolUseId)));
2212
+ }
185
2213
  function logicalSessionId(session) {
186
- return session.sessionGroupId ?? session.sessionId;
2214
+ return session.rootRunId;
187
2215
  }
188
2216
  function experienceSessionGroupKey(session) {
189
2217
  const logicalId = logicalSessionId(session);
190
- if (session.traceRole && session.traceRole !== 'standalone' && session.sessionGroupPath) {
191
- return `group:${session.sessionGroupPath}\u0000${logicalId}`;
192
- }
193
- return `trace:${session.sourcePath}`;
194
- }
195
- function groupSessionsByLogicalId(sessions) {
196
- const groups = new Map();
197
- for (const session of sessions) {
198
- const key = logicalSessionId(session);
199
- const group = groups.get(key) ?? [];
200
- group.push(session);
201
- groups.set(key, group);
2218
+ if (session.role !== 'standalone' && session.groupPath) {
2219
+ return `group:${session.groupPath}\u0000${logicalId}`;
202
2220
  }
203
- for (const [key, group] of groups.entries()) {
204
- groups.set(key, group.sort(compareSessionsForTimeline));
205
- }
206
- return groups;
2221
+ return `trace:${session.traceId}`;
207
2222
  }
208
2223
  function groupSessionsByExperienceKey(sessions) {
209
2224
  const groups = new Map();
@@ -219,14 +2234,15 @@ function groupSessionsByExperienceKey(sessions) {
219
2234
  return groups;
220
2235
  }
221
2236
  function compareSessionsForTimeline(a, b) {
222
- const roleRank = (session) => session.traceRole === 'main' ? 0 : session.traceRole === 'standalone' ? 1 : 2;
2237
+ const roleRank = (session) => session.role === 'main' ? 0 : session.role === 'standalone' ? 1 : 2;
223
2238
  const rank = roleRank(a) - roleRank(b);
224
2239
  if (rank !== 0)
225
2240
  return rank;
226
2241
  const time = (a.startTimestamp ?? '').localeCompare(b.startTimestamp ?? '');
227
2242
  if (time !== 0)
228
2243
  return time;
229
- return a.sourcePath.localeCompare(b.sourcePath);
2244
+ return a.sourcePath.localeCompare(b.sourcePath)
2245
+ || a.traceId.localeCompare(b.traceId);
230
2246
  }
231
2247
  function timestampsOverlap(aStart, aEnd, bStart, bEnd) {
232
2248
  if (!aStart || !aEnd || !bStart || !bEnd)
@@ -235,45 +2251,58 @@ function timestampsOverlap(aStart, aEnd, bStart, bEnd) {
235
2251
  }
236
2252
  function segmentRecordBounds(session, segment) {
237
2253
  if (typeof segment.startRecordIndex === 'number' && typeof segment.endRecordIndex === 'number') {
238
- const rawStart = clampRecordIndex(session, segment.startRecordIndex);
239
- const rawEnd = clampRecordIndex(session, Math.max(segment.startRecordIndex, segment.endRecordIndex));
2254
+ const rawStart = eventIndexForSourceIndex(session, segment.startRecordIndex, 'first');
2255
+ const rawEnd = eventIndexForSourceIndex(session, Math.max(segment.startRecordIndex, segment.endRecordIndex), 'last');
240
2256
  const humanStart = Math.min(rawStart, previousHumanUserRecordIndex(session, rawStart) ?? rawStart);
241
2257
  return {
242
2258
  start: includeLeadingRuntimeContext(session, humanStart),
243
- end: includeTrailingDeliveryContext(session, Math.min(session.records.length, rawEnd + 1)),
2259
+ end: includeTrailingDeliveryContext(session, Math.min(session.events.length, rawEnd + 1)),
244
2260
  };
245
2261
  }
246
2262
  const indexes = segment.toolCalls
247
2263
  .map((toolCall) => toolCall.messageIndex)
248
2264
  .filter((index) => typeof index === 'number' && index >= 0);
249
2265
  if (indexes.length > 0) {
250
- const start = Math.max(0, Math.min(...indexes) - 3);
2266
+ const firstEventIndex = eventIndexForSourceIndex(session, Math.min(...indexes), 'first');
2267
+ const lastEventIndex = eventIndexForSourceIndex(session, Math.max(...indexes), 'last');
2268
+ const start = Math.max(0, firstEventIndex - 3);
251
2269
  return {
252
2270
  start: Math.min(start, previousHumanUserRecordIndex(session, start) ?? start),
253
- end: includeTrailingDeliveryContext(session, Math.min(session.records.length, Math.max(...indexes) + 5)),
2271
+ end: includeTrailingDeliveryContext(session, Math.min(session.events.length, lastEventIndex + 5)),
254
2272
  };
255
2273
  }
256
2274
  const timestampIndexes = [];
257
- session.records.forEach((record, index) => {
258
- const ts = timestampOf(record);
2275
+ session.events.forEach((event, index) => {
2276
+ const ts = event.timestamp;
259
2277
  if (ts && ts >= segment.startTimestamp && ts <= segment.endTimestamp)
260
2278
  timestampIndexes.push(index);
261
2279
  });
262
2280
  if (timestampIndexes.length === 0)
263
- return { start: 0, end: Math.min(session.records.length, 12) };
2281
+ return { start: 0, end: Math.min(session.events.length, 12) };
264
2282
  const start = Math.max(0, Math.min(...timestampIndexes) - 2);
265
2283
  return {
266
2284
  start: Math.min(start, previousHumanUserRecordIndex(session, start) ?? start),
267
- end: includeTrailingDeliveryContext(session, Math.min(session.records.length, Math.max(...timestampIndexes) + 3)),
2285
+ end: includeTrailingDeliveryContext(session, Math.min(session.events.length, Math.max(...timestampIndexes) + 3)),
268
2286
  };
269
2287
  }
270
2288
  function clampRecordIndex(session, index) {
271
- return Math.max(0, Math.min(session.records.length - 1, index));
2289
+ return Math.max(0, Math.min(session.events.length - 1, index));
2290
+ }
2291
+ function eventIndexForSourceIndex(session, sourceIndex, edge) {
2292
+ if (edge === 'first') {
2293
+ const index = session.events.findIndex((event) => event.sourceIndex >= sourceIndex);
2294
+ return index < 0 ? clampRecordIndex(session, sourceIndex) : index;
2295
+ }
2296
+ for (let index = session.events.length - 1; index >= 0; index -= 1) {
2297
+ if (session.events[index].sourceIndex <= sourceIndex)
2298
+ return index;
2299
+ }
2300
+ return 0;
272
2301
  }
273
2302
  function includeLeadingRuntimeContext(session, start) {
274
2303
  let nextStart = start;
275
2304
  for (let index = start - 1; index >= 0; index -= 1) {
276
- const events = timelineEventsFromRecord(session, session.records[index], index);
2305
+ const events = timelineEventsFromTraceEvent(session, session.events[index], index);
277
2306
  if (events.length === 0)
278
2307
  continue;
279
2308
  if (events.some((event) => event.kind === 'runtime_context')) {
@@ -285,10 +2314,10 @@ function includeLeadingRuntimeContext(session, start) {
285
2314
  return nextStart;
286
2315
  }
287
2316
  function includeTrailingDeliveryContext(session, end) {
288
- const safeEnd = Math.min(session.records.length, Math.max(0, end));
289
- const lookaheadEnd = Math.min(session.records.length, safeEnd + 8);
2317
+ const safeEnd = Math.min(session.events.length, Math.max(0, end));
2318
+ const lookaheadEnd = Math.min(session.events.length, safeEnd + 8);
290
2319
  for (let index = safeEnd; index < lookaheadEnd; index += 1) {
291
- const events = timelineEventsFromRecord(session, session.records[index], index);
2320
+ const events = timelineEventsFromTraceEvent(session, session.events[index], index);
292
2321
  if (events.some((event) => event.kind === 'user_message'))
293
2322
  break;
294
2323
  if (events.some(isAssistantDeliveryEvent))
@@ -297,8 +2326,8 @@ function includeTrailingDeliveryContext(session, end) {
297
2326
  return safeEnd;
298
2327
  }
299
2328
  function previousHumanUserRecordIndex(session, start) {
300
- for (let index = Math.min(start, session.records.length - 1); index >= 0; index -= 1) {
301
- const events = timelineEventsFromRecord(session, session.records[index], index);
2329
+ for (let index = Math.min(start, session.events.length - 1); index >= 0; index -= 1) {
2330
+ const events = timelineEventsFromTraceEvent(session, session.events[index], index);
302
2331
  if (events.some((event) => event.kind === 'user_message'))
303
2332
  return index;
304
2333
  }
@@ -334,195 +2363,183 @@ function hasRepeatedExecutionSignal(event) {
334
2363
  const text = `${event.label ?? ''} ${event.toolName ?? ''} ${event.fullText ?? event.snippet ?? ''}`;
335
2364
  return /重复(?:执行|尝试|读取|搜索|调用)|再次(?:执行|读取|搜索|调用)|重新(?:执行|读取|搜索|调用|跑|运行)|再(?:执行|读取|搜索|调用|跑)一遍|重试|\b(?:retry|rerun)\b/i.test(text);
336
2365
  }
337
- function buildTimeline(session, start, end) {
338
- return buildTimelineWindow(session, start, end).slice(0, TIMELINE_PREVIEW_EVENT_LIMIT);
2366
+ function buildInvocationTimeline(session, start, end, segment) {
2367
+ const window = buildTimelineWindow(session, start, end);
2368
+ const callInstanceIds = new Set(segment.toolCalls.flatMap((toolCall) => toolCall.callInstanceId ? [toolCall.callInstanceId] : []));
2369
+ const legacyCallIds = new Set(segment.toolCalls.flatMap((toolCall) => !toolCall.callInstanceId && toolCall.toolUseId ? [toolCall.toolUseId] : []));
2370
+ if (callInstanceIds.size === 0 && legacyCallIds.size === 0)
2371
+ return window;
2372
+ const linkedResults = session.events.flatMap((event, eventIndex) => event.eventKind === 'tool_result'
2373
+ && (event.callInstanceId
2374
+ ? callInstanceIds.has(event.callInstanceId)
2375
+ : legacyCallIds.has(event.callId))
2376
+ && (eventIndex < start || eventIndex >= end)
2377
+ ? timelineEventsFromTraceEvent(session, event, eventIndex)
2378
+ : []);
2379
+ return uniqueTimelineEvents([...window, ...linkedResults]).sort(compareTimelineEvents);
339
2380
  }
340
2381
  function buildTimelineWindow(session, start, end) {
341
2382
  const events = [];
342
- const safeEnd = Math.min(session.records.length, Math.max(start, end));
2383
+ const safeEnd = Math.min(session.events.length, Math.max(start, end));
343
2384
  for (let index = start; index < safeEnd; index += 1) {
344
- events.push(...timelineEventsFromRecord(session, session.records[index], index));
2385
+ events.push(...timelineEventsFromTraceEvent(session, session.events[index], index));
345
2386
  }
346
- return events
347
- .sort((a, b) => a.order - b.order);
2387
+ return events.sort((a, b) => a.order - b.order);
348
2388
  }
349
- function timelineEventsFromRecord(session, record, messageIndex) {
350
- if (!record || typeof record !== 'object')
351
- return [];
352
- const rec = record;
353
- const uuid = typeof rec.uuid === 'string' ? rec.uuid : undefined;
354
- const timestamp = timestampOf(record);
2389
+ function timelineEventsFromTraceEvent(session, event, eventIndex) {
2390
+ const messageIndex = event.sourceIndex;
355
2391
  const base = {
2392
+ traceId: session.traceId,
356
2393
  sourceTrace: session.sourcePath,
357
2394
  sessionId: logicalSessionId(session),
358
- traceRole: session.traceRole,
359
- traceLabel: session.traceLabel,
2395
+ traceRole: session.role,
2396
+ traceLabel: session.label,
360
2397
  messageIndex,
361
2398
  logicalMessageIndex: messageIndex,
362
2399
  sourceLineIndex: messageIndex,
363
- messageUuid: uuid,
364
- timestamp,
2400
+ messageUuid: event.sourceEventId ?? event.eventId,
2401
+ timestamp: event.timestamp,
365
2402
  };
366
- if (rec.type === 'user') {
367
- const user = rec;
368
- return userEvents(user, base, messageIndex);
2403
+ const order = eventIndex * 10;
2404
+ if (event.eventKind === 'message' && event.role === 'user') {
2405
+ return userTimelineEvents(event, base, order);
369
2406
  }
370
- if (rec.type === 'assistant') {
371
- const assistant = rec;
372
- return assistantEvents(assistant, base, messageIndex);
373
- }
374
- return sessionEventFromRecord(rec, base, messageIndex);
375
- }
376
- function sessionEventFromRecord(record, base, messageIndex) {
377
- const type = typeof record.type === 'string' ? record.type : '';
378
- const rawText = safeRecordText(record);
379
- if (!isSessionBoundaryType(type) && !hasAssistantTurnFailedText(rawText))
380
- return [];
381
- return [timelineEvent({
382
- ...base,
383
- kind: 'runtime_context',
384
- role: 'other',
385
- order: messageIndex * 10,
386
- snippet: snippet(rawText || type, 700),
387
- fullText: fullText(rawText || type),
388
- label: type || 'session event',
389
- })];
390
- }
391
- function isSessionBoundaryType(type) {
392
- return /^session[._-](?:started|ended)$/i.test(type);
393
- }
394
- function safeRecordText(record) {
395
- try {
396
- return JSON.stringify(record);
2407
+ if (event.eventKind === 'message' && event.role === 'system') {
2408
+ return [timelineEvent({
2409
+ ...base,
2410
+ kind: 'runtime_context',
2411
+ role: 'tool',
2412
+ order,
2413
+ snippet: snippet(event.text, 700),
2414
+ fullText: fullText(event.text),
2415
+ label: 'system context',
2416
+ })];
397
2417
  }
398
- catch {
399
- return String(record ?? '');
2418
+ if (event.eventKind === 'message' && event.role === 'assistant') {
2419
+ const protocolReply = isAssistantProtocolReplyText(event.text);
2420
+ return [timelineEvent({
2421
+ ...base,
2422
+ kind: protocolReply ? 'runtime_context' : 'assistant_message',
2423
+ role: 'assistant',
2424
+ order,
2425
+ snippet: snippet(event.text, 700),
2426
+ fullText: fullText(event.text),
2427
+ label: protocolReply ? 'assistant protocol reply' : 'assistant message',
2428
+ })];
400
2429
  }
401
- }
402
- function hasAssistantTurnFailedText(value) {
403
- return /\[assistant turn failed\]|assistant turn failed|assistant_turn_failed|turn failed/i.test(value);
404
- }
405
- function userEvents(record, base, messageIndex) {
406
- const content = record.message.content;
407
- const events = [];
408
- if (typeof content === 'string') {
409
- events.push(...userTextEventsFromText(record, content, base, messageIndex, 0));
410
- return events;
2430
+ if (event.eventKind === 'tool_call') {
2431
+ const inputText = JSON.stringify(event.input);
2432
+ return [timelineEvent({
2433
+ ...base,
2434
+ kind: 'tool_use',
2435
+ role: 'assistant',
2436
+ order,
2437
+ callInstanceId: event.callInstanceId,
2438
+ toolUseId: event.callId,
2439
+ toolName: event.tool.displayName ?? event.tool.name,
2440
+ snippet: snippet(inputText, 900),
2441
+ fullText: fullText(inputText),
2442
+ label: `tool_use ${event.tool.displayName ?? event.tool.name}`,
2443
+ })];
411
2444
  }
412
- let textIndex = 0;
413
- let resultIndex = 0;
414
- for (const part of content) {
415
- if (part.type === 'text') {
416
- const nextEvents = userTextEventsFromText(record, part.text, base, messageIndex, textIndex);
417
- events.push(...nextEvents);
418
- if (nextEvents.length > 0)
419
- textIndex += nextEvents.length;
420
- }
421
- else if (part.type === 'tool_result') {
422
- const full = fullText(part.content);
423
- const text = snippet(part.content, 900);
424
- const failed = part.is_error === true || isToolResultFailureText(part.content);
425
- events.push(timelineEvent({
2445
+ if (event.eventKind === 'tool_result') {
2446
+ const failed = event.status === 'failure';
2447
+ return [timelineEvent({
426
2448
  ...base,
427
2449
  kind: 'tool_result',
428
2450
  role: 'tool',
429
- order: messageIndex * 10 + 5 + resultIndex,
430
- toolUseId: part.tool_use_id,
2451
+ order,
2452
+ callInstanceId: event.callInstanceId,
2453
+ toolUseId: event.callId,
2454
+ toolStatus: event.status,
431
2455
  isError: failed,
432
- snippet: text,
433
- fullText: full,
434
- label: failed ? 'tool result error' : 'tool result',
435
- }));
436
- resultIndex += 1;
437
- }
2456
+ snippet: snippet(event.output, 900),
2457
+ fullText: fullText(event.output),
2458
+ label: failed
2459
+ ? 'tool result error'
2460
+ : event.status === 'cancelled' ? 'tool result cancelled'
2461
+ : event.status === 'unknown' ? 'tool result status unknown' : 'tool result',
2462
+ })];
438
2463
  }
439
- return events;
2464
+ if (event.eventKind === 'lifecycle') {
2465
+ return [timelineEvent({
2466
+ ...base,
2467
+ kind: 'runtime_context',
2468
+ role: 'other',
2469
+ order,
2470
+ snippet: snippet(`${event.phase}${event.reason ? `: ${event.reason}` : ''}`, 700),
2471
+ fullText: fullText(JSON.stringify(event)),
2472
+ label: event.phase,
2473
+ })];
2474
+ }
2475
+ if (event.eventKind === 'unknown') {
2476
+ const rawText = safeRecordText(event.raw);
2477
+ if (!hasAssistantTurnFailedText(rawText))
2478
+ return [];
2479
+ return [timelineEvent({
2480
+ ...base,
2481
+ kind: 'runtime_context',
2482
+ role: 'other',
2483
+ order,
2484
+ snippet: snippet(rawText, 700),
2485
+ fullText: fullText(rawText),
2486
+ label: event.sourceType,
2487
+ })];
2488
+ }
2489
+ return [];
440
2490
  }
441
- function userTextEventsFromText(record, rawText, base, messageIndex, offset) {
2491
+ function userTimelineEvents(event, base, order) {
442
2492
  const events = [];
443
- const commandEnvelope = extractCommandEnvelopeText(rawText);
2493
+ const commandEnvelope = extractCommandEnvelopeText(event.text);
444
2494
  if (commandEnvelope) {
445
2495
  events.push(timelineEvent({
446
2496
  ...base,
447
2497
  kind: 'runtime_context',
448
2498
  role: 'tool',
449
- order: messageIndex * 10 + offset,
2499
+ order,
450
2500
  snippet: snippet(commandEnvelope, 700),
451
2501
  fullText: fullText(commandEnvelope),
452
2502
  label: 'command envelope',
453
2503
  }));
454
2504
  }
455
- const humanText = commandEnvelope ? stripCommandEnvelopeText(rawText) : rawText;
456
- const full = fullText(humanText);
457
- const text = snippet(humanText, 700);
458
- if (text) {
459
- const kind = userTextEventKind(record, text);
460
- events.push(timelineEvent({
461
- ...base,
462
- kind,
463
- role: kind === 'user_message' ? 'user' : kind === 'synthetic_user_event' ? 'other' : 'tool',
464
- order: messageIndex * 10 + offset + (commandEnvelope ? 1 : 0),
465
- snippet: text,
466
- fullText: full,
467
- label: userTextEventLabel(kind),
468
- }));
469
- }
2505
+ const text = commandEnvelope ? stripCommandEnvelopeText(event.text) : event.text;
2506
+ if (!text.trim())
2507
+ return events;
2508
+ const kind = event.origin === 'human'
2509
+ ? 'user_message'
2510
+ : event.origin === 'skill-context'
2511
+ ? 'skill_context'
2512
+ : event.origin === 'synthetic'
2513
+ ? 'synthetic_user_event'
2514
+ : 'runtime_context';
2515
+ events.push(timelineEvent({
2516
+ ...base,
2517
+ kind,
2518
+ role: kind === 'user_message' ? 'user' : kind === 'synthetic_user_event' ? 'other' : 'tool',
2519
+ order: order + (commandEnvelope ? 1 : 0),
2520
+ snippet: snippet(text, 700),
2521
+ fullText: fullText(text),
2522
+ label: userTextEventLabel(kind),
2523
+ }));
470
2524
  return events;
471
2525
  }
472
- function assistantEvents(record, base, messageIndex) {
473
- const events = [];
474
- const textParts = [];
475
- let index = 0;
476
- for (const part of record.message.content) {
477
- if (part.type === 'text' && part.text)
478
- textParts.push(part.text);
479
- if (part.type === 'tool_use' && part.id && part.name) {
480
- const inputText = JSON.stringify(part.input ?? {});
481
- events.push(timelineEvent({
482
- ...base,
483
- kind: 'tool_use',
484
- role: 'assistant',
485
- order: messageIndex * 10 + 5 + index,
486
- toolUseId: part.id,
487
- toolName: part.name,
488
- snippet: snippet(inputText, 900),
489
- fullText: fullText(inputText),
490
- label: `tool_use ${part.name}`,
491
- }));
492
- index += 1;
493
- }
2526
+ function safeRecordText(record) {
2527
+ try {
2528
+ return JSON.stringify(record);
494
2529
  }
495
- const rawText = textParts.join('\n');
496
- const text = snippet(rawText, 700);
497
- if (text) {
498
- const protocolReply = isAssistantProtocolReplyText(rawText);
499
- events.unshift(timelineEvent({
500
- ...base,
501
- kind: protocolReply ? 'runtime_context' : 'assistant_message',
502
- role: 'assistant',
503
- order: messageIndex * 10,
504
- snippet: text,
505
- fullText: fullText(rawText),
506
- label: protocolReply ? 'assistant protocol reply' : 'assistant message',
507
- }));
2530
+ catch {
2531
+ return String(record ?? '');
508
2532
  }
509
- return events;
2533
+ }
2534
+ function hasAssistantTurnFailedText(value) {
2535
+ return /\[assistant turn failed\]|assistant turn failed|assistant_turn_failed|turn failed/i.test(value);
510
2536
  }
511
2537
  function timelineEvent(input) {
512
2538
  return {
513
2539
  ...input,
514
- id: hashParts(input.sourceTrace, input.sessionId, String(input.messageIndex ?? ''), input.kind, input.toolUseId ?? '', input.snippet ?? ''),
2540
+ id: hashParts(input.traceId ?? '', input.sourceTrace, input.sessionId, input.messageUuid ?? '', String(input.messageIndex ?? ''), input.kind, input.callInstanceId ?? '', input.toolUseId ?? '', input.snippet ?? ''),
515
2541
  };
516
2542
  }
517
- function userTextEventKind(record, text) {
518
- if (isSkillContextRecord(record, text))
519
- return 'skill_context';
520
- if (isRuntimeContextRecord(record, text))
521
- return 'runtime_context';
522
- if (isSyntheticUserMessageText(text))
523
- return 'synthetic_user_event';
524
- return 'user_message';
525
- }
526
2543
  function userTextEventLabel(kind) {
527
2544
  if (kind === 'skill_context')
528
2545
  return 'skill context';
@@ -536,12 +2553,14 @@ function observationEvidenceRef(item) {
536
2553
  return {
537
2554
  id: hashParts('observation', item.id),
538
2555
  kind: 'observation',
2556
+ traceId: item.traceId ?? item.evidence.traceId,
539
2557
  sourceTrace: item.sourceTrace,
540
2558
  sessionId: item.sessionId,
541
2559
  messageIndex: item.evidence.messageIndex,
542
2560
  logicalMessageIndex: item.evidence.messageIndex,
543
2561
  sourceLineIndex: item.evidence.messageIndex,
544
2562
  messageUuid: item.evidence.messageUuid,
2563
+ callInstanceId: item.evidence.callInstanceId,
545
2564
  toolUseId: item.evidence.toolUseId,
546
2565
  timestamp: item.evidence.segmentTimestamp,
547
2566
  role: 'other',
@@ -553,6 +2572,7 @@ function evidenceRefFromTimeline(event) {
553
2572
  return {
554
2573
  id: event.id,
555
2574
  kind: event.kind,
2575
+ traceId: event.traceId,
556
2576
  sourceTrace: event.sourceTrace,
557
2577
  sessionId: event.sessionId,
558
2578
  traceRole: event.traceRole,
@@ -561,6 +2581,7 @@ function evidenceRefFromTimeline(event) {
561
2581
  logicalMessageIndex: event.logicalMessageIndex ?? event.messageIndex,
562
2582
  sourceLineIndex: event.sourceLineIndex ?? event.messageIndex,
563
2583
  messageUuid: event.messageUuid,
2584
+ callInstanceId: event.callInstanceId,
564
2585
  toolUseId: event.toolUseId,
565
2586
  timestamp: event.timestamp,
566
2587
  role: event.role,
@@ -709,14 +2730,27 @@ function indicatorsForSegment(segment, relatedItems, timeline, metricScopeId, re
709
2730
  repeatedExecutionCount: timeline.reduce((sum, ref) => sum + (metricIsActive(ref, 'repeated_execution', hasRepeatedExecutionSignal(ref), reviewState) ? 1 : 0), 0),
710
2731
  toolCallCount: segment.metrics.numToolCalls,
711
2732
  toolFailureCount: Math.max(segment.metrics.numToolFailures, timeline.filter(isToolFailureEvent).length),
2733
+ toolCancelledCount: segment.metrics.numToolCancelled,
2734
+ toolUnknownCount: segment.metrics.numToolUnknown,
712
2735
  highObservationCount: relatedItems.filter((item) => item.severity === 'high').length,
713
2736
  mediumObservationCount: relatedItems.filter((item) => item.severity === 'medium').length,
714
- hedgingCount: relatedItems.filter((item) => item.signalType === 'hedging').reduce((sum, item) => sum + item.occurrences, 0),
715
- explicitMarkerCount: relatedItems.filter((item) => item.signalType === 'explicit_marker').reduce((sum, item) => sum + item.occurrences, 0),
2737
+ hedgingCount: sumSafeCounts(...relatedItems
2738
+ .filter((item) => item.signalType === 'hedging')
2739
+ .map((item) => item.occurrences)),
2740
+ explicitMarkerCount: sumSafeCounts(...relatedItems
2741
+ .filter((item) => item.signalType === 'explicit_marker')
2742
+ .map((item) => item.occurrences)),
716
2743
  };
717
2744
  }
718
2745
  function isToolFailureEvent(event) {
719
- return event.kind === 'tool_result' && (event.isError === true || isToolResultFailureText(event.fullText ?? event.snippet ?? ''));
2746
+ if (event.kind !== 'tool_result')
2747
+ return false;
2748
+ if (event.toolStatus !== undefined)
2749
+ return event.toolStatus === 'failure';
2750
+ return event.isError === true || isToolResultFailureText(event.fullText ?? event.snippet ?? '');
2751
+ }
2752
+ function isSessionInterruptedType(type) {
2753
+ return /^turn[._-](?:aborted|interrupted)$/i.test(type);
720
2754
  }
721
2755
  function sessionInterruptedEvents(timeline) {
722
2756
  const events = uniqueTimelineEvents(timeline).sort(compareTimelineEvents);
@@ -724,7 +2758,7 @@ function sessionInterruptedEvents(timeline) {
724
2758
  for (let index = 0; index < events.length; index += 1) {
725
2759
  const event = events[index];
726
2760
  const text = `${event.label ?? ''}\n${event.snippet ?? ''}\n${event.fullText ?? ''}`;
727
- if (hasAssistantTurnFailedText(text)) {
2761
+ if (isSessionInterruptedType(event.label ?? '') || hasAssistantTurnFailedText(text)) {
728
2762
  interrupted.push(event);
729
2763
  continue;
730
2764
  }
@@ -755,6 +2789,7 @@ function metricCount(ref, metricKey, ruleCount, reviewState, metricScopeId) {
755
2789
  function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedAt, reviewState) {
756
2790
  const byKey = new Map();
757
2791
  const canonicalEpisodesBySessionGroup = new Map();
2792
+ const timelineBySessionGroup = new Map();
758
2793
  for (const invocation of invocations) {
759
2794
  const key = `${invocation.skillName}\u0000${invocation.sessionGroupKey}`;
760
2795
  const group = byKey.get(key) ?? [];
@@ -771,40 +2806,44 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
771
2806
  const sessionGroup = sessionGroupsByKey.get(first.sessionGroupKey) ?? [];
772
2807
  const sourceSessionStartTimestamp = minString(sessionGroup.map((session) => session.startTimestamp));
773
2808
  const sourceSessionEndTimestamp = maxString(sessionGroup.map((session) => session.endTimestamp));
774
- const timelineTree = sessionGroup.length > 0 ? buildSessionTimelineTree(first.sessionId, sessionGroup) : undefined;
775
- const fullSessionTimeline = timelineTree
776
- ? uniqueTimelineEvents([
777
- ...timelineTree.main,
778
- ...timelineTree.branches.flatMap((branch) => branch.events),
779
- ]).sort(compareTimelineEvents)
780
- : timeline;
2809
+ const timestampedInvocations = group.filter(invocationTimestampObserved);
2810
+ const timestampedInvocationCount = timestampedInvocations.length;
2811
+ const invocationStartTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? first.startTimestamp;
2812
+ const invocationEndTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? first.endTimestamp;
2813
+ let cachedTimeline = timelineBySessionGroup.get(first.sessionGroupKey);
2814
+ if (!cachedTimeline) {
2815
+ const tree = sessionGroup.length > 0
2816
+ ? buildSessionTimelineTree(first.sessionId, sessionGroup)
2817
+ : undefined;
2818
+ cachedTimeline = {
2819
+ tree,
2820
+ flattened: tree ? flattenTimelineTree(tree) : timeline,
2821
+ };
2822
+ timelineBySessionGroup.set(first.sessionGroupKey, cachedTimeline);
2823
+ }
2824
+ const timelineTree = cachedTimeline.tree;
2825
+ const fullSessionTimeline = cachedTimeline.flattened;
781
2826
  const fullSessionEventCount = fullSessionTimeline.length;
782
2827
  const previewEvents = timeline.slice(0, TIMELINE_PREVIEW_EVENT_LIMIT);
783
- const previewIndexes = previewEvents
784
- .map((event) => event.messageIndex)
2828
+ const fullSessionPositionById = new Map(fullSessionTimeline.map((event, index) => [event.id, index]));
2829
+ const previewPositions = previewEvents
2830
+ .map((event) => fullSessionPositionById.get(event.id))
785
2831
  .filter((index) => typeof index === 'number');
786
- const groupStartRecordIndex = minDefined(group.map((invocation) => minDefined(invocation.timeline.map((event) => event.messageIndex))));
787
- const groupEndRecordIndex = maxDefined(group.map((invocation) => maxDefined(invocation.timeline.map((event) => event.messageIndex))));
788
- const previewStartRecordIndex = minDefined(previewIndexes);
789
- const previewEndRecordIndex = maxDefined(previewIndexes);
790
- const fullSessionIndexes = fullSessionTimeline
791
- .map((event) => event.messageIndex)
792
- .filter((index) => typeof index === 'number');
793
- const sessionStartRecordIndex = minDefined(fullSessionIndexes) ?? 0;
794
- const sessionEndRecordIndex = maxDefined(fullSessionIndexes) ?? Math.max(0, (sessionGroup[0]?.records.length ?? 1) - 1);
795
- const omittedBeforeCount = previewStartRecordIndex === undefined
796
- ? 0
797
- : fullSessionTimeline.filter((event) => typeof event.messageIndex === 'number' && event.messageIndex < previewStartRecordIndex).length;
798
- const omittedAfterCount = previewEndRecordIndex === undefined
799
- ? 0
800
- : fullSessionTimeline.filter((event) => typeof event.messageIndex === 'number' && event.messageIndex > previewEndRecordIndex).length;
2832
+ const omittedBeforeCount = previewPositions.length > 0
2833
+ ? Math.min(...previewPositions)
2834
+ : 0;
2835
+ const omittedAfterCount = previewPositions.length > 0
2836
+ ? fullSessionTimeline.length - 1 - Math.max(...previewPositions)
2837
+ : 0;
801
2838
  const observationRefs = uniqueEvidenceRefs(group.flatMap((invocation) => invocation.evidenceRefs.filter((ref) => ref.kind === 'observation')));
802
2839
  const evidenceChain = evidenceChainForTimeline(timeline, observationRefs);
803
2840
  const metricScopeId = hashParts('session', first.skillName, first.sessionGroupKey);
804
2841
  const ruleFindings = ruleFindingsForEvidence(indicators, timeline, observationRefs, evidenceChain, metricScopeId);
805
2842
  const assistiveInference = assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings);
806
2843
  const problemPatterns = mergeExperienceProblemPatterns(group.flatMap((invocation) => invocation.problemPatterns));
807
- const storyInvocations = invocations.filter((invocation) => invocation.sessionGroupKey === first.sessionGroupKey);
2844
+ const storyInvocations = invocations
2845
+ .filter((invocation) => invocation.sessionGroupKey === first.sessionGroupKey)
2846
+ .sort(compareStoryInvocations);
808
2847
  const baseSession = {
809
2848
  id: metricScopeId,
810
2849
  skillName: first.skillName,
@@ -817,8 +2856,10 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
817
2856
  sourceSessionStartTimestamp,
818
2857
  sourceSessionEndTimestamp,
819
2858
  sourceSessionDurationMs: durationMsBetween(sourceSessionStartTimestamp, sourceSessionEndTimestamp),
820
- startTimestamp: group.reduce((min, invocation) => invocation.startTimestamp < min ? invocation.startTimestamp : min, first.startTimestamp),
821
- endTimestamp: group.reduce((max, invocation) => invocation.endTimestamp > max ? invocation.endTimestamp : max, first.endTimestamp),
2859
+ startTimestamp: invocationStartTimestamp,
2860
+ endTimestamp: invocationEndTimestamp,
2861
+ timestampedInvocationCount,
2862
+ timestampCoverage: group.length > 0 ? timestampedInvocationCount / group.length : 0,
822
2863
  invocationIds: group.map((invocation) => invocation.id),
823
2864
  goalSliceIds: unique(group.map((invocation) => invocation.goalSliceId)),
824
2865
  reviewPriority: priorityForScore(reviewPriorityScore),
@@ -830,19 +2871,19 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
830
2871
  assistiveInference,
831
2872
  problemPatterns,
832
2873
  relatedObservationIds,
2874
+ timelineRef: timelineRefForSessionGroup(first.sessionGroupKey),
2875
+ timelinePreviewEventIds: previewEvents.map((event) => event.id),
833
2876
  timelinePreview: previewEvents,
834
2877
  fullSessionTimeline,
835
2878
  timelineTree,
836
2879
  timelineScope: {
837
2880
  mode: 'skill_segment_window',
838
- segmentStartRecordIndex: groupStartRecordIndex,
839
- segmentEndRecordIndex: groupEndRecordIndex,
840
- previewStartRecordIndex,
841
- previewEndRecordIndex,
842
- sessionStartRecordIndex,
843
- sessionEndRecordIndex,
2881
+ segmentEventCount: timeline.length,
844
2882
  previewEventCount: previewEvents.length,
845
2883
  fullSessionEventCount,
2884
+ segmentRecordRanges: traceRecordRanges(timeline),
2885
+ previewRecordRanges: traceRecordRanges(previewEvents),
2886
+ sessionRecordRanges: traceRecordRanges(fullSessionTimeline),
846
2887
  truncated: timeline.length > previewEvents.length || omittedBeforeCount > 0 || omittedAfterCount > 0,
847
2888
  omittedBeforeCount,
848
2889
  omittedAfterCount,
@@ -852,17 +2893,28 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
852
2893
  rawSkillRefs: unique(group.map((invocation) => invocation.attribution.rawSkillRef).filter((value) => Boolean(value))).sort(),
853
2894
  commandNames: unique(group.map((invocation) => invocation.attribution.commandName).filter((value) => Boolean(value))).sort(),
854
2895
  };
855
- const sessionStory = buildSessionStory(baseSession, storyInvocations, canonicalEpisodesBySessionGroup.get(first.sessionGroupKey), reviewState);
856
- if (!canonicalEpisodesBySessionGroup.has(first.sessionGroupKey)) {
857
- canonicalEpisodesBySessionGroup.set(first.sessionGroupKey, sessionStory.episodes ?? []);
2896
+ let canonicalEpisodes = canonicalEpisodesBySessionGroup.get(first.sessionGroupKey);
2897
+ if (!canonicalEpisodes) {
2898
+ const canonicalSession = canonicalStorySession(baseSession, storyInvocations);
2899
+ canonicalEpisodes = buildSessionStory(canonicalSession, storyInvocations, undefined, reviewState).episodes ?? [];
2900
+ canonicalEpisodesBySessionGroup.set(first.sessionGroupKey, canonicalEpisodes);
858
2901
  }
2902
+ const sessionStory = {
2903
+ ...buildSessionStory(baseSession, storyInvocations, canonicalEpisodes, reviewState),
2904
+ contextRef: storyContextRefForSessionGroup(first.sessionGroupKey),
2905
+ };
859
2906
  const sessionWithStoryBase = {
860
2907
  ...baseSession,
861
2908
  sessionStory,
862
2909
  };
2910
+ const enrichedIndicators = enrichRouterDownstreamIndicators(sessionWithStoryBase);
2911
+ const enrichedReviewPriorityScore = scoreForIndicators(enrichedIndicators);
863
2912
  const sessionWithStory = {
864
2913
  ...sessionWithStoryBase,
865
- indicators: enrichRouterDownstreamIndicators(sessionWithStoryBase),
2914
+ indicators: enrichedIndicators,
2915
+ reviewPriority: priorityForScore(enrichedReviewPriorityScore),
2916
+ reviewPriorityScore: enrichedReviewPriorityScore,
2917
+ reviewBasisCodes: basisCodesForIndicators(enrichedIndicators),
866
2918
  };
867
2919
  const reviewerReport = buildReviewerReport(sessionWithStory, group, generatedAt, reviewState, storyInvocations, sessionStory);
868
2920
  return {
@@ -876,9 +2928,67 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
876
2928
  return priorityDiff;
877
2929
  if (b.reviewPriorityScore !== a.reviewPriorityScore)
878
2930
  return b.reviewPriorityScore - a.reviewPriorityScore;
2931
+ const timestampCoverageDiff = sessionTimestampedInvocationCount(b) - sessionTimestampedInvocationCount(a);
2932
+ if (timestampCoverageDiff !== 0)
2933
+ return timestampCoverageDiff;
879
2934
  return b.endTimestamp.localeCompare(a.endTimestamp);
880
2935
  });
881
2936
  }
2937
+ function compareStoryInvocations(left, right) {
2938
+ return left.startTimestamp.localeCompare(right.startTimestamp)
2939
+ || left.sourceTrace.localeCompare(right.sourceTrace)
2940
+ || left.segmentIndex - right.segmentIndex
2941
+ || left.skillName.localeCompare(right.skillName)
2942
+ || left.id.localeCompare(right.id);
2943
+ }
2944
+ function canonicalStorySession(base, invocations) {
2945
+ const first = invocations[0];
2946
+ const indicators = sumIndicators(invocations.map((invocation) => invocation.indicators));
2947
+ const relatedObservationIds = unique(invocations.flatMap((invocation) => invocation.relatedObservationIds));
2948
+ const observationRefs = uniqueEvidenceRefs(invocations.flatMap((invocation) => invocation.evidenceRefs.filter((ref) => ref.kind === 'observation')));
2949
+ const evidenceChain = evidenceChainForTimeline(base.fullSessionTimeline, observationRefs);
2950
+ const reviewPriorityScore = scoreForIndicators(indicators);
2951
+ const id = hashParts('session-story', first?.sessionGroupKey ?? base.sessionId);
2952
+ const timestampedInvocations = invocations.filter(invocationTimestampObserved);
2953
+ const ruleFindings = ruleFindingsForEvidence(indicators, base.fullSessionTimeline, observationRefs, evidenceChain, id);
2954
+ return {
2955
+ ...base,
2956
+ id,
2957
+ skillName: first?.skillName ?? base.skillName,
2958
+ sourceTrace: base.fullSessionTimeline.find((event) => event.traceRole === 'main' || event.traceRole === 'standalone')?.sourceTrace ?? first?.sourceTrace ?? base.sourceTrace,
2959
+ sourceKind: first?.sourceKind ?? base.sourceKind,
2960
+ entrypoint: first?.entrypoint ?? base.entrypoint,
2961
+ sourceMetadata: mergeSourceMetadata(invocations.map((invocation) => invocation.sourceMetadata)),
2962
+ cwd: first?.cwd ?? base.cwd,
2963
+ startTimestamp: minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? base.startTimestamp,
2964
+ endTimestamp: maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? base.endTimestamp,
2965
+ timestampedInvocationCount: timestampedInvocations.length,
2966
+ timestampCoverage: invocations.length > 0
2967
+ ? timestampedInvocations.length / invocations.length
2968
+ : 0,
2969
+ invocationIds: invocations.map((invocation) => invocation.id),
2970
+ goalSliceIds: unique(invocations.map((invocation) => invocation.goalSliceId)),
2971
+ reviewPriority: priorityForScore(reviewPriorityScore),
2972
+ reviewPriorityScore,
2973
+ reviewBasisCodes: basisCodesForIndicators(indicators),
2974
+ indicators,
2975
+ evidenceChain,
2976
+ ruleFindings,
2977
+ assistiveInference: assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings),
2978
+ problemPatterns: mergeExperienceProblemPatterns(invocations.flatMap((invocation) => invocation.problemPatterns)),
2979
+ relatedObservationIds,
2980
+ attributionSources: unique(invocations.map((invocation) => invocation.attribution.source).filter(Boolean)).sort(),
2981
+ pluginNames: unique(invocations
2982
+ .map((invocation) => invocation.attribution.pluginName)
2983
+ .filter((value) => Boolean(value))).sort(),
2984
+ rawSkillRefs: unique(invocations
2985
+ .map((invocation) => invocation.attribution.rawSkillRef)
2986
+ .filter((value) => Boolean(value))).sort(),
2987
+ commandNames: unique(invocations
2988
+ .map((invocation) => invocation.attribution.commandName)
2989
+ .filter((value) => Boolean(value))).sort(),
2990
+ };
2991
+ }
882
2992
  function experiencePriorityRank(priority) {
883
2993
  if (priority === 'review_first')
884
2994
  return 2;
@@ -926,6 +3036,8 @@ function buildReviewerReport(session, invocations, generatedAt, reviewState, sto
926
3036
  oneLookMetrics: {
927
3037
  toolCallCount: indicators.toolCallCount,
928
3038
  toolFailureCount: indicators.toolFailureCount,
3039
+ toolCancelledCount: indicators.toolCancelledCount ?? 0,
3040
+ toolUnknownCount: indicators.toolUnknownCount ?? 0,
929
3041
  userMessageCount: indicators.userMessageCount,
930
3042
  userFollowUpCount: feedbackCounts.userFollowUpCount,
931
3043
  assistantDeliverySignalCount: indicators.assistantDeliverySignalCount,
@@ -982,8 +3094,13 @@ function buildSessionStory(session, invocations, canonicalEpisodes, reviewState)
982
3094
  if (subagentDispatches.length > 0) {
983
3095
  subagentNode = push('subagent_branch', '分支 / 子任务', 'unknown', `检测到 ${subagentDispatches.length} 条分支或子任务执行线;主线和分支已单独列出,仍需要结合原文确认真实委派关系。`, subagentDispatches.flatMap((dispatch) => dispatch.evidenceRefs.slice(0, 1)));
984
3096
  }
985
- const executionNode = push('tool_execution', '执行过程', session.indicators.toolFailureCount > 0 ? 'attention' : session.indicators.toolCallCount > 0 ? 'ok' : 'unknown', session.indicators.toolCallCount > 0
986
- ? `执行中看到 ${session.indicators.toolCallCount} 次工具调用${session.indicators.toolFailureCount > 0 ? `,其中失败 ${session.indicators.toolFailureCount} 次` : ''}。`
3097
+ const executionNode = push('tool_execution', '执行过程', session.indicators.toolFailureCount > 0
3098
+ ? 'attention'
3099
+ : (session.indicators.toolCancelledCount ?? 0) > 0
3100
+ || (session.indicators.toolUnknownCount ?? 0) > 0
3101
+ ? 'unknown'
3102
+ : session.indicators.toolCallCount > 0 ? 'ok' : 'unknown', session.indicators.toolCallCount > 0
3103
+ ? executionOutcomeText(session.indicators)
987
3104
  : '没有看到明确工具调用;只能根据消息上下文复盘。', uniqueEvidenceRefs([
988
3105
  session.evidenceChain.firstToolUse,
989
3106
  session.evidenceChain.firstToolFailure,
@@ -1025,7 +3142,7 @@ function buildSessionStory(session, invocations, canonicalEpisodes, reviewState)
1025
3142
  schemaVersion: 1,
1026
3143
  summary,
1027
3144
  invocationCount: invocations.length,
1028
- goalSliceCount: session.goalSliceIds.length,
3145
+ goalSliceCount: goalSlices.length,
1029
3146
  branchCount: subagentDispatches.length,
1030
3147
  progressUpdateCount: progressUpdates.length,
1031
3148
  finalDeliverySignalCount: finalDeliveries.length,
@@ -1058,17 +3175,23 @@ function sessionStoryEpisodes(session, invocations, goalSlices, subagentDispatch
1058
3175
  const ranges = sessionStoryEpisodeRanges(session, timeline);
1059
3176
  return ranges.map((range, index) => {
1060
3177
  const episodeId = hashParts('session-story-episode', session.id, String(index));
1061
- const episodeSkillSegments = skillSegments.filter((segment) => (segment.messageRanges ?? []).some((messageRange) => messageRangeOverlapsEpisodeRange(messageRange, range)));
1062
- const segmentIds = new Set(episodeSkillSegments.map((segment) => segment.id));
3178
+ const rangedSkillSegments = skillSegments.filter((segment) => (segment.messageRanges ?? []).some((messageRange) => messageRangeOverlapsEpisodeRange(messageRange, range)));
3179
+ const rangedSegmentIds = new Set(rangedSkillSegments.map((segment) => segment.id));
1063
3180
  const episodeEdges = orchestrationEdges
1064
3181
  .filter((edge) => {
1065
3182
  if (edge.edgeKind === 'internal_skill' && edge.parentSkillSegmentId && edge.executorSkillSegmentId) {
1066
- return segmentIds.has(edge.parentSkillSegmentId) && segmentIds.has(edge.executorSkillSegmentId);
3183
+ return rangedSegmentIds.has(edge.parentSkillSegmentId) && rangedSegmentIds.has(edge.executorSkillSegmentId);
1067
3184
  }
1068
- return (edge.parentSkillSegmentId && segmentIds.has(edge.parentSkillSegmentId))
3185
+ return (edge.parentSkillSegmentId && rangedSegmentIds.has(edge.parentSkillSegmentId))
1069
3186
  || edge.evidenceRefs.some((ref) => episodeRangeContainsRef(range, ref));
1070
3187
  })
1071
3188
  .map((edge) => ({ ...edge, episodeId }));
3189
+ const linkedSegmentIds = new Set(episodeEdges.flatMap((edge) => [
3190
+ edge.parentSkillSegmentId,
3191
+ edge.executorSkillSegmentId,
3192
+ ].filter((id) => Boolean(id))));
3193
+ const episodeSkillSegments = skillSegments.filter((segment) => rangedSegmentIds.has(segment.id) || linkedSegmentIds.has(segment.id));
3194
+ const segmentIds = new Set(episodeSkillSegments.map((segment) => segment.id));
1072
3195
  const episodeFeedbackSignals = feedbackSignals
1073
3196
  .filter((signal) => episodeRangeContainsRef(range, signal.evidenceRef))
1074
3197
  .map((signal) => {
@@ -1162,17 +3285,31 @@ function skillSegmentsWithOrchestrationRoles(skillSegments, orchestrationEdges)
1162
3285
  }
1163
3286
  function sessionStoryEpisodeRanges(session, timeline) {
1164
3287
  const primarySourceTrace = primarySourceTraceForSession(session);
1165
- const rangeTimeline = timeline.filter((event) => !primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace);
3288
+ const primaryTraceId = primaryTraceIdForSession(session, primarySourceTrace);
3289
+ const rangeTimeline = timeline.filter((event) => primaryTraceId
3290
+ ? !event.traceId || event.traceId === primaryTraceId
3291
+ : !primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace);
1166
3292
  const indexes = rangeTimeline
1167
3293
  .map((event) => event.messageIndex)
1168
3294
  .filter((value) => typeof value === 'number');
1169
- const sessionStart = minDefined(indexes) ?? session.timelineScope.sessionStartRecordIndex ?? 0;
1170
- const sessionEnd = maxDefined(indexes) ?? session.timelineScope.sessionEndRecordIndex ?? sessionStart;
3295
+ const persistedRange = session.timelineScope.sessionRecordRanges?.find((range) => primaryTraceId
3296
+ ? range.traceId === primaryTraceId
3297
+ : range.sourceTrace === primarySourceTrace);
3298
+ const sessionStart = minDefined(indexes)
3299
+ ?? persistedRange?.startRecordIndex
3300
+ ?? session.timelineScope.sessionStartRecordIndex
3301
+ ?? 0;
3302
+ const sessionEnd = maxDefined(indexes)
3303
+ ?? persistedRange?.endRecordIndex
3304
+ ?? session.timelineScope.sessionEndRecordIndex
3305
+ ?? sessionStart;
1171
3306
  const goalShiftStarts = unique(timeline
1172
3307
  .filter((event) => event.kind === 'user_message'
1173
3308
  && typeof event.messageIndex === 'number'
1174
3309
  && event.messageIndex > sessionStart
1175
- && (!primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace))
3310
+ && (primaryTraceId
3311
+ ? !event.traceId || event.traceId === primaryTraceId
3312
+ : !primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace))
1176
3313
  .filter((event) => hasUserGoalShiftSignal(event.snippet ?? event.fullText ?? ''))
1177
3314
  .map((event) => event.messageIndex))
1178
3315
  .sort((a, b) => a - b);
@@ -1180,11 +3317,20 @@ function sessionStoryEpisodeRanges(session, timeline) {
1180
3317
  return starts.map((start, index) => ({
1181
3318
  startMessageIndex: start,
1182
3319
  endMessageIndex: (starts[index + 1] ?? (sessionEnd + 1)) - 1,
3320
+ traceId: primaryTraceId,
1183
3321
  sourceTrace: primarySourceTrace,
1184
3322
  sessionId: session.sessionId,
1185
3323
  boundaryReason: index < starts.length - 1 ? 'goal_shift' : undefined,
1186
3324
  }));
1187
3325
  }
3326
+ function primaryTraceIdForSession(session, primarySourceTrace = primarySourceTraceForSession(session)) {
3327
+ const timeline = session.fullSessionTimeline.length > 0
3328
+ ? session.fullSessionTimeline
3329
+ : session.timelinePreview;
3330
+ return timeline.find((event) => event.traceRole === 'main'
3331
+ && (!primarySourceTrace || event.sourceTrace === primarySourceTrace))?.traceId
3332
+ ?? timeline.find((event) => !primarySourceTrace || event.sourceTrace === primarySourceTrace)?.traceId;
3333
+ }
1188
3334
  function primarySourceTraceForSession(session) {
1189
3335
  return session.sourceTrace
1190
3336
  ?? session.evidenceChain.firstUserMessage?.sourceTrace
@@ -1195,8 +3341,8 @@ function primarySourceTraceForSession(session) {
1195
3341
  function episodeRangeContainsRef(range, ref) {
1196
3342
  if (!ref || typeof ref.messageIndex !== 'number')
1197
3343
  return false;
1198
- // New observations carry sourceTrace and are compared strictly. Older persisted
1199
- // observations may not, so they keep the historical messageIndex-only fallback.
3344
+ if (range.traceId && ref.traceId && range.traceId !== ref.traceId)
3345
+ return false;
1200
3346
  if (range.sourceTrace && ref.sourceTrace && range.sourceTrace !== ref.sourceTrace)
1201
3347
  return false;
1202
3348
  if (range.sessionId && ref.sessionId && range.sessionId !== ref.sessionId)
@@ -1204,6 +3350,8 @@ function episodeRangeContainsRef(range, ref) {
1204
3350
  return ref.messageIndex >= range.startMessageIndex && ref.messageIndex <= range.endMessageIndex;
1205
3351
  }
1206
3352
  function messageRangeOverlapsEpisodeRange(messageRange, range) {
3353
+ if (range.traceId && messageRange.traceId && range.traceId !== messageRange.traceId)
3354
+ return false;
1207
3355
  if (range.sourceTrace && messageRange.sourceTrace && range.sourceTrace !== messageRange.sourceTrace)
1208
3356
  return false;
1209
3357
  if (range.sessionId && messageRange.sessionId && range.sessionId !== messageRange.sessionId)
@@ -1239,10 +3387,11 @@ function sessionStorySkillSegments(session, invocations, skillLinks) {
1239
3387
  .filter((value) => typeof value === 'number');
1240
3388
  const startMessageIndex = minDefined(indexes);
1241
3389
  const endMessageIndex = maxDefined(indexes);
3390
+ const traceId = invocation.traceId ?? timelineWithIndex[0]?.traceId;
1242
3391
  const sourceTrace = invocation.sourceTrace ?? timelineWithIndex[0]?.sourceTrace;
1243
3392
  const sessionId = invocation.sessionId ?? timelineWithIndex[0]?.sessionId;
1244
3393
  return typeof startMessageIndex === 'number' && typeof endMessageIndex === 'number'
1245
- ? { startMessageIndex, endMessageIndex, sourceTrace, sessionId }
3394
+ ? { startMessageIndex, endMessageIndex, traceId, sourceTrace, sessionId }
1246
3395
  : undefined;
1247
3396
  })
1248
3397
  .filter((value) => Boolean(value));
@@ -1305,16 +3454,8 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
1305
3454
  const mentionedUpstream = runnerOwner
1306
3455
  ? mentionedUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timeline, skillSegments)
1307
3456
  : undefined;
1308
- const runnerText = `${runnerEvent?.toolName ?? ''} ${runnerEvent?.snippet ?? ''} ${runnerEvent?.fullText ?? ''}`;
1309
- const runnerExecutor = runnerText
1310
- ? (mentionedUpstream && runnerOwner && runnerOwner.id !== mentionedUpstream.id
1311
- ? runnerOwner
1312
- : skillSegments.find((segment) => {
1313
- if (segment.id === runnerOwner?.id || segment.id === mentionedUpstream?.id)
1314
- return false;
1315
- const name = segment.skillName.toLowerCase();
1316
- return name.includes('apply-cc') && /apply-cc|runner\.js|send-input\.js/i.test(runnerText);
1317
- }))
3457
+ const runnerExecutor = mentionedUpstream && runnerOwner && runnerOwner.id !== mentionedUpstream.id
3458
+ ? runnerOwner
1318
3459
  : undefined;
1319
3460
  const priorUpstreamCandidate = runnerOwner
1320
3461
  ? bestPriorUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timeline, skillSegments)
@@ -1330,25 +3471,30 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
1330
3471
  && segment.id !== router?.id
1331
3472
  && segment.episodeRole === 'main_executor') : undefined);
1332
3473
  if (parentSegment && (executor || runnerEvent) && parentSegment.id !== executor?.id) {
1333
- const edgeKind = executor && skillSegmentsShareSourceTrace(parentSegment, executor)
3474
+ const edgeKind = executor && skillSegmentsSharePhysicalTrace(parentSegment, executor)
1334
3475
  ? 'internal_skill'
1335
3476
  : 'external_child_session';
1336
- edges.push({
1337
- id: hashParts('session-story-edge', episodeId, parentSegment.id, executor?.id ?? 'downstream', '0'),
1338
- episodeId,
1339
- edgeKind,
1340
- parentSkillSegmentId: parentSegment.id,
1341
- executorSkillSegmentId: executor?.id,
1342
- childSessionId: sessionStoryChildSessionId(runnerEvent),
1343
- runnerStartedRef: runnerRef,
1344
- status: runnerEvent || subagentDispatches.length > 0 ? 'started' : 'unknown',
1345
- evidenceRefs: uniqueEvidenceRefs([
1346
- ...parentSegment.evidenceRefs.slice(0, 2),
1347
- ...(executor?.evidenceRefs.slice(0, 2) ?? []),
1348
- ...(runnerRef ? [runnerRef] : []),
1349
- ...subagentDispatches.flatMap((dispatch) => dispatch.evidenceRefs.slice(0, 1)),
1350
- ]).slice(0, 6),
1351
- });
3477
+ // A concrete branch below is the authoritative external-child edge.
3478
+ // Keep this inferred edge only for an internal handoff or when no branch
3479
+ // trace was captured, otherwise the same launch appears twice.
3480
+ if (edgeKind === 'internal_skill' || subagentDispatches.length === 0) {
3481
+ edges.push({
3482
+ id: hashParts('session-story-edge', episodeId, parentSegment.id, executor?.id ?? 'downstream', '0'),
3483
+ episodeId,
3484
+ edgeKind,
3485
+ parentSkillSegmentId: parentSegment.id,
3486
+ executorSkillSegmentId: executor?.id,
3487
+ childSessionId: sessionStoryChildSessionId(runnerEvent),
3488
+ runnerStartedRef: runnerRef,
3489
+ status: runnerEvent || subagentDispatches.length > 0 ? 'started' : 'unknown',
3490
+ evidenceRefs: uniqueEvidenceRefs([
3491
+ ...parentSegment.evidenceRefs.slice(0, 2),
3492
+ ...(executor?.evidenceRefs.slice(0, 2) ?? []),
3493
+ ...(runnerRef ? [runnerRef] : []),
3494
+ ...subagentDispatches.flatMap((dispatch) => dispatch.evidenceRefs.slice(0, 1)),
3495
+ ]).slice(0, 6),
3496
+ });
3497
+ }
1352
3498
  }
1353
3499
  if (edges.length === 0) {
1354
3500
  const fallbackEdge = fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocations);
@@ -1356,17 +3502,30 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
1356
3502
  edges.push(fallbackEdge);
1357
3503
  }
1358
3504
  for (const dispatch of subagentDispatches) {
1359
- const parentSegment = delegator ?? router;
1360
- const executor = skillSegments.find((segment) => segment.id !== parentSegment?.id && segment.episodeRole === 'main_executor');
3505
+ const parentSegment = delegator ?? router ?? runnerOwner;
3506
+ const executor = skillSegments.find((segment) => segment.id !== parentSegment?.id
3507
+ && segment.evidenceRefs.some((ref) => ref.traceId === dispatch.traceId)) ?? skillSegments.find((segment) => segment.id !== parentSegment?.id && segment.episodeRole === 'main_executor');
3508
+ const dispatchRunnerRef = runnerRef && (!dispatch.attachTo?.callInstanceId
3509
+ ? !dispatch.attachTo?.toolUseId
3510
+ || dispatch.attachTo.toolUseId === runnerRef.toolUseId
3511
+ : dispatch.attachTo.callInstanceId === runnerRef.callInstanceId)
3512
+ ? runnerRef
3513
+ : undefined;
1361
3514
  edges.push({
1362
3515
  id: hashParts('session-story-edge', episodeId, dispatch.id),
1363
3516
  episodeId,
1364
- edgeKind: executor ? 'internal_skill' : 'external_child_session',
3517
+ edgeKind: 'external_child_session',
1365
3518
  parentSkillSegmentId: parentSegment?.id,
1366
3519
  executorSkillSegmentId: executor?.id,
1367
- childSessionId: dispatch.branchId,
3520
+ childSessionId: dispatch.childSessionId,
3521
+ runnerStartedRef: dispatchRunnerRef,
1368
3522
  status: 'started',
1369
- evidenceRefs: dispatch.evidenceRefs,
3523
+ evidenceRefs: uniqueEvidenceRefs([
3524
+ ...(parentSegment?.evidenceRefs.slice(0, 2) ?? []),
3525
+ ...(executor?.evidenceRefs.slice(0, 2) ?? []),
3526
+ ...(dispatchRunnerRef ? [dispatchRunnerRef] : []),
3527
+ ...dispatch.evidenceRefs,
3528
+ ]).slice(0, 6),
1370
3529
  });
1371
3530
  }
1372
3531
  return edges;
@@ -1418,7 +3577,7 @@ function mentionedUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timel
1418
3577
  const compactMentioned = compactName.length >= 6 && compactText.includes(compactName);
1419
3578
  if (!explicitlyMentioned && !compactMentioned)
1420
3579
  return false;
1421
- return /skill|技能|流程|工作流|按|根据|启动|触发|\/consult|\/tech-solution|\/prd_start|runner\.js/i.test(localContext);
3580
+ return /skill|技能|流程|工作流|按|根据|启动|触发|委派|分发|delegate|dispatch|route/i.test(localContext);
1422
3581
  })
1423
3582
  .sort((a, b) => upstreamParentScore(b, runnerOwner, nearbyText) - upstreamParentScore(a, runnerOwner, nearbyText)
1424
3583
  || Math.abs(a.order - runnerOwner.order) - Math.abs(b.order - runnerOwner.order))[0];
@@ -1433,10 +3592,6 @@ function upstreamParentScore(segment, runnerOwner, contextText) {
1433
3592
  score += 70;
1434
3593
  if (segment.episodeRole === 'observer' || segment.skillType === 'advisory')
1435
3594
  score += 45;
1436
- if (/dev[-_\s]*lifecycle|aiprd[-_\s]*task[-_\s]*runner|omk[-_\s]*reviewer/i.test(lowerName))
1437
- score += 35;
1438
- if (/yuque|web[-_\s]*fetch|fetch|search/i.test(lowerName))
1439
- score -= 35;
1440
3595
  const nameIndex = lowerText.indexOf(lowerName);
1441
3596
  if (nameIndex >= 0) {
1442
3597
  const localContext = lowerText.slice(Math.max(0, nameIndex - 50), nameIndex + lowerName.length + 120);
@@ -1451,10 +3606,7 @@ function upstreamParentScore(segment, runnerOwner, contextText) {
1451
3606
  }
1452
3607
  function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocations) {
1453
3608
  const timeline = uniqueTimelineEvents(invocations.flatMap((invocation) => invocation.timeline)).sort(compareTimelineEvents);
1454
- const runtimeEvent = timeline.find((event) => {
1455
- const text = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
1456
- return /skills\/[\w.-]+\/scripts\/|runner\.js|send-input\.js|check-session\.js|session:\s*claude-|ttydUrl/i.test(text);
1457
- });
3609
+ const runtimeEvent = timeline.find(isOrchestrationRuntimeEvent);
1458
3610
  if (!runtimeEvent)
1459
3611
  return undefined;
1460
3612
  const runtimeRef = evidenceRefFromTimeline(runtimeEvent);
@@ -1466,7 +3618,7 @@ function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocati
1466
3618
  .sort((a, b) => b.order - a.order)[0];
1467
3619
  if (!parent)
1468
3620
  return undefined;
1469
- const edgeKind = skillSegmentsShareSourceTrace(parent, executor) ? 'internal_skill' : 'external_child_session';
3621
+ const edgeKind = skillSegmentsSharePhysicalTrace(parent, executor) ? 'internal_skill' : 'external_child_session';
1470
3622
  return {
1471
3623
  id: hashParts('session-story-edge', episodeId, parent.id, executor.id, 'fallback'),
1472
3624
  episodeId,
@@ -1486,22 +3638,28 @@ function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocati
1486
3638
  function isOrchestrationRuntimeEvent(event) {
1487
3639
  const text = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
1488
3640
  if (event.kind === 'tool_use') {
1489
- return /runner\.js|send-input\.js|check-session\.js/i.test(text);
3641
+ return /^(?:Task|Agent)$/i.test(event.toolName ?? '')
3642
+ || /runner\.js|send-input\.js|check-session\.js/i.test(text);
1490
3643
  }
1491
3644
  if (event.kind === 'tool_result') {
1492
3645
  const trimmed = text.trim();
1493
3646
  if (/^---\s*\n?\s*name:/i.test(trimmed) || /^---\s+name:/i.test(trimmed))
1494
3647
  return false;
1495
- return /"event"\s*:\s*"started"|Command still running \(session|Process exited with code|Process exited with signal|session:\s*claude-|\bclaude-[a-z0-9_-]+\b/i.test(text);
3648
+ return /"event"\s*:\s*"started"|Command still running \(session|Process exited with code|Process exited with signal/i.test(text)
3649
+ || /["']?(?:child_?session_?id|agent_?id|thread_?id|session_?id)["']?\s*[:=]/i.test(text)
3650
+ || /\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*[a-z0-9][a-z0-9._-]*/i.test(text);
1496
3651
  }
1497
3652
  if (event.kind === 'assistant_message') {
1498
- return /子\s*Claude|ttyd|session:\s*claude-|已启动.*Claude|Claude\s*执行窗口/i.test(text);
3653
+ return /ttyd|(?:已启动|启动了|spawned|started).*(?:subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex))|(?:subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex)).*(?:执行|运行|分析|started|running)/i.test(text)
3654
+ || /\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*[a-z0-9][a-z0-9._-]*/i.test(text);
1499
3655
  }
1500
3656
  return false;
1501
3657
  }
1502
3658
  function sessionStoryChildSessionId(event) {
1503
3659
  const text = `${event?.snippet ?? ''} ${event?.fullText ?? ''}`;
1504
- return text.match(/\bclaude-[a-z0-9_-]+\b/i)?.[0];
3660
+ return text.match(/["']?(?:child_?session_?id|agent_?id|thread_?id|session_?id)["']?\s*[:=]\s*["']?([a-z0-9][a-z0-9._-]*)/i)?.[1]
3661
+ ?? text.match(/\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*([a-z0-9][a-z0-9._-]*)/i)?.[1]
3662
+ ?? text.match(/\b(?:claude|codex|agent|subagent)-[a-z0-9_-]+\b/i)?.[0];
1505
3663
  }
1506
3664
  function sessionStoryFeedbackSignals(session, invocations, skillSegments, orchestrationEdges) {
1507
3665
  const timeline = uniqueTimelineEvents(invocations.flatMap((invocation) => invocation.timeline))
@@ -1579,7 +3737,7 @@ function feedbackAttributionsForText(text, evidenceRef, skillSegments, orchestra
1579
3737
  : targetOwner
1580
3738
  ? { segment: targetOwner, reason: 'object_match' }
1581
3739
  : actionOwner
1582
- ? { segment: actionOwner, reason: 'action_match' }
3740
+ ? actionOwner
1583
3741
  : windowOwner
1584
3742
  ? { segment: windowOwner, reason: feedbackAttributionReasonForText(lower) }
1585
3743
  : undefined;
@@ -1652,8 +3810,6 @@ function upstreamPromiseOwnerScore(segment) {
1652
3810
  score += 80;
1653
3811
  if (segment.episodeRole === 'delegator' || segment.skillType === 'delegation')
1654
3812
  score += 70;
1655
- if (/aiprd[-_\s]*task[-_\s]*runner|dev[-_\s]*lifecycle|omk[-_\s]*reviewer/i.test(segment.skillName))
1656
- score += 30;
1657
3813
  return score;
1658
3814
  }
1659
3815
  function downstreamRelatedParentsForPrimary(primary, skillSegments, orchestrationEdges) {
@@ -1718,21 +3874,19 @@ function targetObjectSkillOwner(text, skillSegments) {
1718
3874
  });
1719
3875
  if (explicitSkill)
1720
3876
  return explicitSkill;
1721
- const byName = (patterns) => skillSegments.find((segment) => patterns.some((pattern) => pattern.test(segment.skillName.toLowerCase())));
1722
- if (/skill\s*extract|soft[-_\s]*standard|llm[-_\s]*enhanced|omk[-_\s]*reviewer|skill\.md|这个skill|执行流程|依赖.*脚本|删除.*脚本|review/.test(lower)) {
1723
- return byName([/omk[-_\s]*reviewer/, /reviewer/]);
1724
- }
1725
- if (/preview|预览|可预览|端口|localhost|127\.0\.0\.1|链接发给我|url/.test(lower)) {
1726
- return byName([/ai[-_\s]*worker[-_\s]*webtools/, /webtools/, /preview/]);
3877
+ if (/runner|child|subagent|sub-agent|子\s*(?:claude|codex|agent|代理|智能体)|(?:claude|codex|agent)\s*session|ttyd|执行窗口/.test(lower)) {
3878
+ return skillSegments
3879
+ .filter((segment) => segment.episodeRole === 'delegator'
3880
+ || segment.episodeRole === 'router'
3881
+ || segment.skillType === 'delegation'
3882
+ || segment.skillType === 'router'
3883
+ || segment.skillType === 'workflow_owner')
3884
+ .sort((a, b) => upstreamPromiseOwnerScore(b) - upstreamPromiseOwnerScore(a) || a.order - b.order)[0];
1727
3885
  }
1728
- if (/runner|child|子\s*claude|claude\s*session|ttyd|执行窗口/.test(lower)) {
1729
- return byName([/apply[-_\s]*cc/]);
1730
- }
1731
- if (/\bpr\b|pull request|merge|分支|拉下|拉取|代码|项目分枝|项目分支|master/.test(lower)) {
1732
- return byName([/dev[-_\s]*lifecycle/, /antcode/, /git/]);
1733
- }
1734
- if (/日报|每天早上|每天晚上|复盘报告/.test(lower)) {
1735
- return byName([/damai[-_\s]*daily/, /daily/]);
3886
+ if (/skill\s*extract|soft[-_\s]*standard|llm[-_\s]*enhanced|skill\.md|这个skill|执行流程|依赖.*脚本|删除.*脚本|review/.test(lower)) {
3887
+ return skillSegments
3888
+ .filter((segment) => segment.episodeRole === 'observer' || segment.skillType === 'advisory')
3889
+ .sort((a, b) => a.order - b.order)[0];
1736
3890
  }
1737
3891
  return undefined;
1738
3892
  }
@@ -1756,6 +3910,21 @@ function actionOwnerForFeedback(evidenceRef, text, timeline, skillSegments) {
1756
3910
  if (!category || typeof evidenceRef.messageIndex !== 'number')
1757
3911
  return undefined;
1758
3912
  const feedbackMessageIndex = evidenceRef.messageIndex;
3913
+ const commandEnvelopeText = timeline
3914
+ .filter((event) => event.kind === 'runtime_context'
3915
+ && event.messageIndex === feedbackMessageIndex
3916
+ && evidenceRefsShareTraceScope(event, evidenceRef))
3917
+ .map((event) => `${event.snippet ?? ''} ${event.fullText ?? ''}`)
3918
+ .join('\n')
3919
+ .toLowerCase();
3920
+ const commandOwner = skillSegments.find((segment) => {
3921
+ const name = segment.skillName.toLowerCase();
3922
+ const compactName = compactObjectText(name);
3923
+ return commandEnvelopeText.includes(name)
3924
+ || (compactName.length >= 4 && compactObjectText(commandEnvelopeText).includes(compactName));
3925
+ });
3926
+ if (commandOwner)
3927
+ return { segment: commandOwner, reason: 'object_match' };
1759
3928
  const nextRuntimeEvent = timeline.find((event) => {
1760
3929
  if (typeof event.messageIndex !== 'number')
1761
3930
  return false;
@@ -1768,7 +3937,10 @@ function actionOwnerForFeedback(evidenceRef, text, timeline, skillSegments) {
1768
3937
  const eventText = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
1769
3938
  return actionCategoryMatchesRuntimeEvent(category, eventText);
1770
3939
  });
1771
- return nextRuntimeEvent ? skillSegmentForEvidenceRef(evidenceRefFromTimeline(nextRuntimeEvent), skillSegments) : undefined;
3940
+ const runtimeOwner = nextRuntimeEvent
3941
+ ? skillSegmentForEvidenceRef(evidenceRefFromTimeline(nextRuntimeEvent), skillSegments)
3942
+ : undefined;
3943
+ return runtimeOwner ? { segment: runtimeOwner, reason: 'action_match' } : undefined;
1772
3944
  }
1773
3945
  function feedbackActionCategory(text) {
1774
3946
  if (/删除|删掉|remove|delete|rm\s/.test(text))
@@ -1802,7 +3974,7 @@ function actionCategoryMatchesRuntimeEvent(category, eventText) {
1802
3974
  function shouldUseWindowFeedbackOwner(segment, text) {
1803
3975
  if (segment.skillType !== 'delegation' && segment.episodeRole !== 'delegator')
1804
3976
  return true;
1805
- return /子\s*claude|child|runner|ttyd|session|claude|有结论|进度|怎么样了|没返回|通知|同步|跑完|完成了吗/i.test(text);
3977
+ return /子\s*(?:claude|codex|agent|代理|智能体)|subagent|sub-agent|child|runner|ttyd|session|thread|agent|有结论|进度|怎么样了|没返回|通知|同步|跑完|完成了吗/i.test(text);
1806
3978
  }
1807
3979
  function dedupeFeedbackAttributions(attributions) {
1808
3980
  const seen = new Set();
@@ -1817,7 +3989,7 @@ function dedupeFeedbackAttributions(attributions) {
1817
3989
  return out;
1818
3990
  }
1819
3991
  function feedbackAttributionReasonForText(lowerText) {
1820
- if (/pr|pull request|master|分支|组件|产物|文档|报告|demo|链接|地址|文件|项目|skill|agent|claude/.test(lowerText)) {
3992
+ if (/pr|pull request|master|分支|组件|产物|文档|报告|demo|链接|地址|文件|项目|skill|agent|subagent|claude|codex/.test(lowerText)) {
1821
3993
  return 'object_match';
1822
3994
  }
1823
3995
  if (/有结论|进度|没返回|通知|返回|同步|发给|给我|怎么样了|还在线|完成了吗|跑完/.test(lowerText)) {
@@ -1859,6 +4031,8 @@ function skillSegmentForEvidenceRef(evidenceRef, skillSegments, preferredSkillNa
1859
4031
  function messageRangeContainsEvidenceRef(range, ref) {
1860
4032
  if (typeof ref.messageIndex !== 'number')
1861
4033
  return false;
4034
+ if (range.traceId && ref.traceId && range.traceId !== ref.traceId)
4035
+ return false;
1862
4036
  if (range.sourceTrace && ref.sourceTrace && range.sourceTrace !== ref.sourceTrace)
1863
4037
  return false;
1864
4038
  if (range.sessionId && ref.sessionId && range.sessionId !== ref.sessionId)
@@ -1866,12 +4040,15 @@ function messageRangeContainsEvidenceRef(range, ref) {
1866
4040
  return ref.messageIndex >= range.startMessageIndex && ref.messageIndex <= range.endMessageIndex;
1867
4041
  }
1868
4042
  function messageRangeTraceSpecificity(range, ref) {
1869
- return (range.sourceTrace && ref.sourceTrace && range.sourceTrace === ref.sourceTrace ? 2 : 0)
4043
+ return (range.traceId && ref.traceId && range.traceId === ref.traceId ? 4 : 0)
4044
+ + (range.sourceTrace && ref.sourceTrace && range.sourceTrace === ref.sourceTrace ? 2 : 0)
1870
4045
  + (range.sessionId && ref.sessionId && range.sessionId === ref.sessionId ? 1 : 0);
1871
4046
  }
1872
4047
  function evidenceRefsShareTraceScope(a, b) {
1873
4048
  if (!a || !b)
1874
4049
  return true;
4050
+ if (a.traceId && b.traceId && a.traceId !== b.traceId)
4051
+ return false;
1875
4052
  if (a.sourceTrace && b.sourceTrace && a.sourceTrace !== b.sourceTrace)
1876
4053
  return false;
1877
4054
  if (a.sessionId && b.sessionId && a.sessionId !== b.sessionId)
@@ -1881,7 +4058,12 @@ function evidenceRefsShareTraceScope(a, b) {
1881
4058
  function timelineEventSharesTraceScope(event, anchor) {
1882
4059
  return evidenceRefsShareTraceScope(event, anchor);
1883
4060
  }
1884
- function skillSegmentsShareSourceTrace(a, b) {
4061
+ function skillSegmentsSharePhysicalTrace(a, b) {
4062
+ const aTraceIds = new Set((a.messageRanges ?? []).map((range) => range.traceId).filter((value) => Boolean(value)));
4063
+ const bTraceIds = new Set((b.messageRanges ?? []).map((range) => range.traceId).filter((value) => Boolean(value)));
4064
+ if (aTraceIds.size > 0 && bTraceIds.size > 0) {
4065
+ return Array.from(aTraceIds).some((traceId) => bTraceIds.has(traceId));
4066
+ }
1885
4067
  const aTraces = new Set((a.messageRanges ?? []).map((range) => range.sourceTrace).filter((value) => Boolean(value)));
1886
4068
  const bTraces = new Set((b.messageRanges ?? []).map((range) => range.sourceTrace).filter((value) => Boolean(value)));
1887
4069
  if (aTraces.size === 0 || bTraces.size === 0)
@@ -1943,8 +4125,9 @@ function sessionStoryGoalSlices(session, invocations) {
1943
4125
  return Array.from(byId.entries()).map(([id, group], index) => {
1944
4126
  const timeline = uniqueTimelineEvents(group.flatMap((invocation) => invocation.timeline)).sort(compareTimelineEvents);
1945
4127
  const userEvents = timeline.filter((event) => event.kind === 'user_message');
1946
- const startTimestamp = minString(group.map((invocation) => invocation.startTimestamp)) ?? session.startTimestamp;
1947
- const endTimestamp = maxString(group.map((invocation) => invocation.endTimestamp)) ?? session.endTimestamp;
4128
+ const timestampedInvocations = group.filter(invocationTimestampObserved);
4129
+ const startTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? session.startTimestamp;
4130
+ const endTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? session.endTimestamp;
1948
4131
  const hasGoalShift = userEvents.some((event) => hasUserGoalShiftSignal(event.snippet ?? ''));
1949
4132
  const reasonCode = hasGoalShift
1950
4133
  ? 'explicit_user_goal_shift'
@@ -1959,7 +4142,12 @@ function sessionStoryGoalSlices(session, invocations) {
1959
4142
  inferredUserGoal: inferUserGoal(userEvents),
1960
4143
  evidenceRefs: userEvents.slice(0, 3).map(evidenceRefFromTimeline),
1961
4144
  };
1962
- }).sort((a, b) => a.startTimestamp.localeCompare(b.startTimestamp));
4145
+ }).sort((a, b) => {
4146
+ const aObserved = a.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
4147
+ const bObserved = b.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
4148
+ return Number(bObserved) - Number(aObserved)
4149
+ || a.startTimestamp.localeCompare(b.startTimestamp);
4150
+ });
1963
4151
  }
1964
4152
  function sessionStorySubagentDispatches(session) {
1965
4153
  const branches = session.timelineTree?.branches ?? [];
@@ -1967,10 +4155,13 @@ function sessionStorySubagentDispatches(session) {
1967
4155
  id: hashParts('session-story-dispatch', session.id, branch.id),
1968
4156
  order: index + 1,
1969
4157
  branchId: branch.id,
4158
+ childSessionId: branch.sessionId,
4159
+ traceId: branch.traceId ?? branch.sourceTrace,
1970
4160
  label: branch.label,
1971
4161
  sourceTrace: branch.sourceTrace,
1972
4162
  attachTo: branch.attachTo ? {
1973
4163
  messageIndex: branch.attachTo.messageIndex,
4164
+ callInstanceId: branch.attachTo.callInstanceId,
1974
4165
  toolUseId: branch.attachTo.toolUseId,
1975
4166
  label: branch.attachTo.label,
1976
4167
  } : undefined,
@@ -2004,8 +4195,11 @@ function sessionStorySkillLinks(session, invocations) {
2004
4195
  }
2005
4196
  function inferSkillRole(group, allInvocations, session) {
2006
4197
  const hasRoutingSignal = group.some((invocation) => routingEvidenceEvents(invocation).length > 0);
2007
- const hasBranchDispatch = (session.timelineTree?.branches.length ?? 0) > 0 && group.some((invocation) => invocation.timeline.some((event) => event.kind === 'tool_use' && /^(Task|Agent|Skill)$/i.test(event.toolName ?? '')));
2008
- const hasExecutionSignal = group.some((invocation) => invocation.timeline.some((event) => event.kind === 'tool_use' && !/^(Task|Agent|Skill)$/i.test(event.toolName ?? '')));
4198
+ const hasBranchDispatch = (session.timelineTree?.branches.length ?? 0) > 0 && group.some((invocation) => invocation.timeline.some((event) => isOrchestrationRuntimeEvent(event)
4199
+ || isDifferentSkillInvocationEvent(event, invocation.skillName)));
4200
+ const hasExecutionSignal = group.some((invocation) => invocation.timeline.some((event) => event.kind === 'tool_use'
4201
+ && !isOrchestrationRuntimeEvent(event)
4202
+ && !isDifferentSkillInvocationEvent(event, invocation.skillName)));
2009
4203
  if ((hasRoutingSignal || hasBranchDispatch) && allInvocations.length > group.length)
2010
4204
  return hasExecutionSignal ? 'mixed' : 'router';
2011
4205
  if (hasRoutingSignal || hasBranchDispatch)
@@ -2018,10 +4212,35 @@ function routingEvidenceEvents(invocation) {
2018
4212
  return invocation.timeline.filter((event) => {
2019
4213
  if (event.kind !== 'assistant_message' && event.kind !== 'tool_use')
2020
4214
  return false;
4215
+ if (isOrchestrationRuntimeEvent(event))
4216
+ return true;
4217
+ if (isDifferentSkillInvocationEvent(event, invocation.skillName))
4218
+ return true;
2021
4219
  const text = event.fullText ?? event.snippet ?? '';
2022
- return /子\s*Claude|subagent|子任务|分发|委派|调用.+skill|走\s*`?[\w-]+`?\s*skill|\/consult|task-runner/i.test(text);
4220
+ return /subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex)|子任务|分发|委派|路由|delegate|dispatch|route|调用.+skill|走\s*`?[\w-]+`?\s*skill/i.test(text);
2023
4221
  }).slice(0, 3);
2024
4222
  }
4223
+ function isDifferentSkillInvocationEvent(event, currentSkillName) {
4224
+ if (event.kind !== 'tool_use' || !/^Skill$/i.test(event.toolName ?? ''))
4225
+ return false;
4226
+ const text = event.fullText ?? event.snippet ?? '';
4227
+ let targetSkill;
4228
+ try {
4229
+ const input = JSON.parse(text);
4230
+ if (isObjectRecord(input)) {
4231
+ targetSkill = typeof input.skill === 'string'
4232
+ ? input.skill
4233
+ : typeof input.name === 'string'
4234
+ ? input.name
4235
+ : undefined;
4236
+ }
4237
+ }
4238
+ catch {
4239
+ targetSkill = text.match(/["']?(?:skill|name)["']?\s*[:=]\s*["']?([a-z0-9][\w.-]*)/i)?.[1];
4240
+ }
4241
+ return Boolean(targetSkill
4242
+ && targetSkill.trim().toLowerCase() !== currentSkillName.trim().toLowerCase());
4243
+ }
2025
4244
  function skillRoleLabel(role) {
2026
4245
  if (role === 'router')
2027
4246
  return '路由';
@@ -2271,7 +4490,7 @@ function declaredBehaviorChecklistItems(session, episodes) {
2271
4490
  contribution: 'informational',
2272
4491
  reason: hasSkillRead
2273
4492
  ? `看到 ${session.evidenceChain.skillContextCount} 次 SKILL.md 加载事件,可作为能力归因证据。`
2274
- : `日志里没看到 LLM 主动 Read SKILL.md。这次 skill 归因来自:${attributionLabel}。OpenClaw 脚本 / Skill 工具 / slash command 触发时这是常态,不代表 skill 没用上。`,
4493
+ : `日志里没看到 LLM 主动读取 SKILL.md。这次 skill 归因来自:${attributionLabel}。脚本、Skill 工具或命令触发时可能没有显式读取事件,不代表 skill 没用上。`,
2275
4494
  evidenceRefs: [session.evidenceChain.firstSkillContext, session.evidenceChain.firstToolUse],
2276
4495
  }),
2277
4496
  checklistItem({
@@ -2687,18 +4906,28 @@ function skillSelectionStepText(session) {
2687
4906
  function executionStepStatus(session, expectedToolCheck) {
2688
4907
  if (session.indicators.toolFailureCount > 0 || expectedToolCheck.declared && expectedToolCheck.matchedTools.length === 0)
2689
4908
  return 'attention';
4909
+ if ((session.indicators.toolCancelledCount ?? 0) > 0
4910
+ || (session.indicators.toolUnknownCount ?? 0) > 0)
4911
+ return 'unknown';
2690
4912
  if (session.indicators.toolCallCount > 0)
2691
4913
  return 'ok';
2692
4914
  return 'unknown';
2693
4915
  }
2694
4916
  function executionStepText(session, expectedToolCheck = expectedToolCheckForSession(session)) {
2695
- const failures = session.indicators.toolFailureCount > 0 ? `,其中失败 ${session.indicators.toolFailureCount} 次` : '';
2696
4917
  const expected = expectedToolCheck.declared
2697
4918
  ? expectedToolCheck.matchedTools.length > 0
2698
4919
  ? `命中声明的核心工具:${expectedToolCheck.matchedTools.join('、')}。`
2699
4920
  : `但没有命中能力声明的核心工具:${expectedToolCheck.expectedTools.join('、')}。`
2700
4921
  : '';
2701
- return `执行中看到 ${session.indicators.toolCallCount} 次工具调用${failures}。${expected}`;
4922
+ return `${executionOutcomeText(session.indicators)}${expected}`;
4923
+ }
4924
+ function executionOutcomeText(indicators) {
4925
+ const details = [
4926
+ indicators.toolFailureCount > 0 ? `失败 ${indicators.toolFailureCount} 次` : '',
4927
+ (indicators.toolCancelledCount ?? 0) > 0 ? `取消 ${indicators.toolCancelledCount ?? 0} 次` : '',
4928
+ (indicators.toolUnknownCount ?? 0) > 0 ? `状态未知 ${indicators.toolUnknownCount ?? 0} 次` : '',
4929
+ ].filter(Boolean);
4930
+ return `执行中看到 ${indicators.toolCallCount} 次工具调用${details.length > 0 ? `,其中${details.join(',')}` : ''}。`;
2702
4931
  }
2703
4932
  function deliveryStepText(session) {
2704
4933
  const closure = userFacingClosureForSession(session);
@@ -3030,17 +5259,30 @@ function suggestionTextForChecklistItem(key) {
3030
5259
  return suggestions[key];
3031
5260
  }
3032
5261
  function sumTokenUsage(invocations) {
3033
- return invocations.reduce((sum, invocation) => ({
3034
- inputTokens: sum.inputTokens + (invocation.metrics.inputTokens ?? 0),
3035
- outputTokens: sum.outputTokens + (invocation.metrics.outputTokens ?? 0),
3036
- cacheReadTokens: sum.cacheReadTokens + (invocation.metrics.cacheReadTokens ?? 0),
3037
- cacheCreationTokens: sum.cacheCreationTokens + (invocation.metrics.cacheCreationTokens ?? 0),
3038
- }), {
3039
- inputTokens: 0,
3040
- outputTokens: 0,
3041
- cacheReadTokens: 0,
3042
- cacheCreationTokens: 0,
3043
- });
5262
+ const observed = invocations.filter((invocation) => invocation.metrics.tokenUsageObserved);
5263
+ const inputTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.inputTokens));
5264
+ const outputTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.outputTokens));
5265
+ const cacheReadTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.cacheReadTokens));
5266
+ const cacheCreationTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.cacheCreationTokens));
5267
+ const aggregateValid = [
5268
+ inputTokens,
5269
+ outputTokens,
5270
+ cacheReadTokens,
5271
+ cacheCreationTokens,
5272
+ ].every((value) => value !== undefined)
5273
+ && checkedSumTokenCounts(inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens) !== undefined;
5274
+ const observedInvocationCount = aggregateValid ? observed.length : 0;
5275
+ return {
5276
+ inputTokens: inputTokens ?? 0,
5277
+ outputTokens: outputTokens ?? 0,
5278
+ cacheReadTokens: cacheReadTokens ?? 0,
5279
+ cacheCreationTokens: cacheCreationTokens ?? 0,
5280
+ observedInvocationCount,
5281
+ invocationCount: invocations.length,
5282
+ coverage: invocations.length > 0
5283
+ ? Number((observedInvocationCount / invocations.length).toFixed(4))
5284
+ : 1,
5285
+ };
3044
5286
  }
3045
5287
  function summarizeExperienceSkills(sessions, invocations) {
3046
5288
  const bySkill = new Map();
@@ -3050,7 +5292,7 @@ function summarizeExperienceSkills(sessions, invocations) {
3050
5292
  bySkill.set(session.skillName, group);
3051
5293
  }
3052
5294
  const invocationCountBySkill = invocations.reduce((acc, invocation) => {
3053
- acc[invocation.skillName] = (acc[invocation.skillName] ?? 0) + 1;
5295
+ incrementRecordCount(acc, invocation.skillName);
3054
5296
  return acc;
3055
5297
  }, {});
3056
5298
  const invocationGroupBySkill = invocations.reduce((acc, invocation) => {
@@ -3066,9 +5308,15 @@ function summarizeExperienceSkills(sessions, invocations) {
3066
5308
  const evidenceChain = sumEvidenceChains(group.map((session) => session.evidenceChain));
3067
5309
  const ruleFindings = mergeRuleFindings(group.flatMap((session) => session.ruleFindings));
3068
5310
  const problemPatterns = mergeExperienceProblemPatterns(group.flatMap((session) => session.problemPatterns));
5311
+ const timestampedInvocations = skillInvocations.filter(invocationTimestampObserved);
5312
+ const timestampedInvocationCount = timestampedInvocations.length;
5313
+ const firstSeen = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp))
5314
+ ?? first.startTimestamp;
5315
+ const lastSeen = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp))
5316
+ ?? first.endTimestamp;
3069
5317
  return {
3070
5318
  skillName,
3071
- invocationCount: invocationCountBySkill[skillName] ?? 0,
5319
+ invocationCount: ownRecordValue(invocationCountBySkill, skillName) ?? 0,
3072
5320
  sessionCount: group.length,
3073
5321
  sourceKinds: unique(group.map((session) => session.sourceKind)).sort(),
3074
5322
  entrypoints: unique(group.map((session) => session.entrypoint).filter((value) => Boolean(value))).sort(),
@@ -3079,8 +5327,12 @@ function summarizeExperienceSkills(sessions, invocations) {
3079
5327
  rawSkillRefs: unique(skillInvocations.map((invocation) => invocation.attribution.rawSkillRef).filter((value) => Boolean(value))).sort(),
3080
5328
  commandNames: unique(skillInvocations.map((invocation) => invocation.attribution.commandName).filter((value) => Boolean(value))).sort(),
3081
5329
  toolCounts: sumRecordCounts(skillInvocations.map((invocation) => invocation.toolCounts)),
3082
- firstSeen: group.reduce((min, session) => session.startTimestamp < min ? session.startTimestamp : min, first.startTimestamp),
3083
- lastSeen: group.reduce((max, session) => session.endTimestamp > max ? session.endTimestamp : max, first.endTimestamp),
5330
+ firstSeen,
5331
+ lastSeen,
5332
+ timestampedInvocationCount,
5333
+ timestampCoverage: skillInvocations.length > 0
5334
+ ? timestampedInvocationCount / skillInvocations.length
5335
+ : 0,
3084
5336
  reviewFirstSessionCount: group.filter((session) => session.reviewPriority === 'review_first').length,
3085
5337
  sampleReviewSessionCount: group.filter((session) => session.reviewPriority === 'sample_review').length,
3086
5338
  indicators,
@@ -3152,17 +5404,14 @@ function sourceSenderLabel(value) {
3152
5404
  return value.sender ?? value.senderId;
3153
5405
  }
3154
5406
  function scoreForIndicators(indicators) {
3155
- return indicators.highObservationCount * 3
3156
- + indicators.mediumObservationCount
3157
- + indicators.userCorrectionCount * 2
3158
- + indicators.userInterruptionCount * 2
3159
- + indicators.sessionInterruptedCount * 2
3160
- + indicators.negativeFeedbackCount * 2
3161
- + indicators.hardRuleTextHitCount
3162
- + indicators.toolFailureCount
3163
- + indicators.routerDownstreamFailed * 2
3164
- + indicators.hedgingCount
3165
- + indicators.explicitMarkerCount * 2;
5407
+ return sumSafeCounts(weightedCount(indicators.highObservationCount, 3), indicators.mediumObservationCount, weightedCount(indicators.userCorrectionCount, 2), weightedCount(indicators.userInterruptionCount, 2), weightedCount(indicators.sessionInterruptedCount, 2), weightedCount(indicators.negativeFeedbackCount, 2), indicators.hardRuleTextHitCount, indicators.toolFailureCount, weightedCount(indicators.routerDownstreamFailed, 2), indicators.hedgingCount, weightedCount(indicators.explicitMarkerCount, 2));
5408
+ }
5409
+ function weightedCount(count, weight) {
5410
+ const weighted = count * weight;
5411
+ if (!Number.isSafeInteger(weighted) || weighted < 0) {
5412
+ throw new RangeError('Weighted observation count exceeds Number.MAX_SAFE_INTEGER');
5413
+ }
5414
+ return weighted;
3166
5415
  }
3167
5416
  function priorityForScore(score) {
3168
5417
  if (score >= 3)
@@ -3213,14 +5462,14 @@ function basisCodesForIndicators(indicators) {
3213
5462
  function countTools(segment) {
3214
5463
  const counts = {};
3215
5464
  for (const toolCall of segment.toolCalls) {
3216
- counts[toolCall.tool] = (counts[toolCall.tool] ?? 0) + 1;
5465
+ incrementRecordCount(counts, toolCall.tool);
3217
5466
  }
3218
5467
  return counts;
3219
5468
  }
3220
5469
  function countBy(values) {
3221
5470
  const counts = {};
3222
5471
  for (const value of values) {
3223
- counts[value] = (counts[value] ?? 0) + 1;
5472
+ incrementRecordCount(counts, value);
3224
5473
  }
3225
5474
  return counts;
3226
5475
  }
@@ -3228,46 +5477,48 @@ function sumRecordCounts(values) {
3228
5477
  const counts = {};
3229
5478
  for (const value of values) {
3230
5479
  for (const [key, count] of Object.entries(value)) {
3231
- counts[key] = (counts[key] ?? 0) + count;
5480
+ incrementRecordCount(counts, key, count);
3232
5481
  }
3233
5482
  }
3234
5483
  return counts;
3235
5484
  }
3236
5485
  function sumIndicators(values) {
3237
5486
  return values.reduce((acc, value) => ({
3238
- userMessageCount: acc.userMessageCount + value.userMessageCount,
3239
- userFollowUpCount: acc.userFollowUpCount + value.userFollowUpCount,
3240
- userCorrectionCount: acc.userCorrectionCount + value.userCorrectionCount,
3241
- userInterruptionCount: acc.userInterruptionCount + value.userInterruptionCount,
3242
- sessionInterruptedCount: acc.sessionInterruptedCount + (value.sessionInterruptedCount ?? 0),
3243
- negativeFeedbackCount: acc.negativeFeedbackCount + (value.negativeFeedbackCount ?? 0),
3244
- positiveFeedbackCount: acc.positiveFeedbackCount + (value.positiveFeedbackCount ?? 0),
3245
- userGoalShiftCount: acc.userGoalShiftCount + (value.userGoalShiftCount ?? 0),
3246
- hardRuleTextHitCount: acc.hardRuleTextHitCount + value.hardRuleTextHitCount,
3247
- assistantDeliverySignalCount: acc.assistantDeliverySignalCount + (value.assistantDeliverySignalCount ?? 0),
3248
- deliverableArtifactSignalCount: acc.deliverableArtifactSignalCount + (value.deliverableArtifactSignalCount ?? 0),
3249
- routerDownstreamCompleted: acc.routerDownstreamCompleted + (value.routerDownstreamCompleted ?? 0),
3250
- routerDownstreamFailed: acc.routerDownstreamFailed + (value.routerDownstreamFailed ?? 0),
3251
- selfCorrectionCount: acc.selfCorrectionCount + (value.selfCorrectionCount ?? 0),
3252
- repeatedExecutionCount: acc.repeatedExecutionCount + (value.repeatedExecutionCount ?? 0),
3253
- toolCallCount: acc.toolCallCount + value.toolCallCount,
3254
- toolFailureCount: acc.toolFailureCount + value.toolFailureCount,
3255
- highObservationCount: acc.highObservationCount + value.highObservationCount,
3256
- mediumObservationCount: acc.mediumObservationCount + value.mediumObservationCount,
3257
- hedgingCount: acc.hedgingCount + value.hedgingCount,
3258
- explicitMarkerCount: acc.explicitMarkerCount + value.explicitMarkerCount,
5487
+ userMessageCount: sumSafeCounts(acc.userMessageCount, value.userMessageCount),
5488
+ userFollowUpCount: sumSafeCounts(acc.userFollowUpCount, value.userFollowUpCount),
5489
+ userCorrectionCount: sumSafeCounts(acc.userCorrectionCount, value.userCorrectionCount),
5490
+ userInterruptionCount: sumSafeCounts(acc.userInterruptionCount, value.userInterruptionCount),
5491
+ sessionInterruptedCount: sumSafeCounts(acc.sessionInterruptedCount, value.sessionInterruptedCount ?? 0),
5492
+ negativeFeedbackCount: sumSafeCounts(acc.negativeFeedbackCount, value.negativeFeedbackCount ?? 0),
5493
+ positiveFeedbackCount: sumSafeCounts(acc.positiveFeedbackCount, value.positiveFeedbackCount ?? 0),
5494
+ userGoalShiftCount: sumSafeCounts(acc.userGoalShiftCount, value.userGoalShiftCount ?? 0),
5495
+ hardRuleTextHitCount: sumSafeCounts(acc.hardRuleTextHitCount, value.hardRuleTextHitCount),
5496
+ assistantDeliverySignalCount: sumSafeCounts(acc.assistantDeliverySignalCount, value.assistantDeliverySignalCount ?? 0),
5497
+ deliverableArtifactSignalCount: sumSafeCounts(acc.deliverableArtifactSignalCount, value.deliverableArtifactSignalCount ?? 0),
5498
+ routerDownstreamCompleted: sumSafeCounts(acc.routerDownstreamCompleted, value.routerDownstreamCompleted ?? 0),
5499
+ routerDownstreamFailed: sumSafeCounts(acc.routerDownstreamFailed, value.routerDownstreamFailed ?? 0),
5500
+ selfCorrectionCount: sumSafeCounts(acc.selfCorrectionCount, value.selfCorrectionCount ?? 0),
5501
+ repeatedExecutionCount: sumSafeCounts(acc.repeatedExecutionCount, value.repeatedExecutionCount ?? 0),
5502
+ toolCallCount: sumSafeCounts(acc.toolCallCount, value.toolCallCount),
5503
+ toolFailureCount: sumSafeCounts(acc.toolFailureCount, value.toolFailureCount),
5504
+ toolCancelledCount: sumSafeCounts(acc.toolCancelledCount ?? 0, value.toolCancelledCount ?? 0),
5505
+ toolUnknownCount: sumSafeCounts(acc.toolUnknownCount ?? 0, value.toolUnknownCount ?? 0),
5506
+ highObservationCount: sumSafeCounts(acc.highObservationCount, value.highObservationCount),
5507
+ mediumObservationCount: sumSafeCounts(acc.mediumObservationCount, value.mediumObservationCount),
5508
+ hedgingCount: sumSafeCounts(acc.hedgingCount, value.hedgingCount),
5509
+ explicitMarkerCount: sumSafeCounts(acc.explicitMarkerCount, value.explicitMarkerCount),
3259
5510
  }), { ...ZERO_INDICATORS });
3260
5511
  }
3261
5512
  function sumEvidenceChains(values) {
3262
5513
  return values.reduce((acc, value) => ({
3263
- userMessageCount: acc.userMessageCount + (value?.userMessageCount ?? 0),
3264
- runtimeContextCount: acc.runtimeContextCount + (value?.runtimeContextCount ?? 0),
3265
- skillContextCount: acc.skillContextCount + (value?.skillContextCount ?? 0),
3266
- assistantMessageCount: acc.assistantMessageCount + (value?.assistantMessageCount ?? 0),
3267
- toolUseCount: acc.toolUseCount + (value?.toolUseCount ?? 0),
3268
- toolResultCount: acc.toolResultCount + (value?.toolResultCount ?? 0),
3269
- toolFailureResultCount: acc.toolFailureResultCount + (value?.toolFailureResultCount ?? 0),
3270
- observationCount: acc.observationCount + (value?.observationCount ?? 0),
5514
+ userMessageCount: sumSafeCounts(acc.userMessageCount, value?.userMessageCount ?? 0),
5515
+ runtimeContextCount: sumSafeCounts(acc.runtimeContextCount, value?.runtimeContextCount ?? 0),
5516
+ skillContextCount: sumSafeCounts(acc.skillContextCount, value?.skillContextCount ?? 0),
5517
+ assistantMessageCount: sumSafeCounts(acc.assistantMessageCount, value?.assistantMessageCount ?? 0),
5518
+ toolUseCount: sumSafeCounts(acc.toolUseCount, value?.toolUseCount ?? 0),
5519
+ toolResultCount: sumSafeCounts(acc.toolResultCount, value?.toolResultCount ?? 0),
5520
+ toolFailureResultCount: sumSafeCounts(acc.toolFailureResultCount, value?.toolFailureResultCount ?? 0),
5521
+ observationCount: sumSafeCounts(acc.observationCount, value?.observationCount ?? 0),
3271
5522
  firstUserMessage: acc.firstUserMessage ?? value?.firstUserMessage,
3272
5523
  firstRuntimeContext: acc.firstRuntimeContext ?? value?.firstRuntimeContext,
3273
5524
  firstSkillContext: acc.firstSkillContext ?? value?.firstSkillContext,
@@ -3290,7 +5541,7 @@ function mergeRuleFindings(values) {
3290
5541
  for (const value of values) {
3291
5542
  const existing = byCode.get(value.code);
3292
5543
  if (existing) {
3293
- existing.count += value.count;
5544
+ existing.count = sumSafeCounts(existing.count, value.count);
3294
5545
  existing.evidenceRefs = uniqueEvidenceRefs([...existing.evidenceRefs, ...value.evidenceRefs]).slice(0, 5);
3295
5546
  }
3296
5547
  else {
@@ -3318,8 +5569,11 @@ function compareTimelineEvents(a, b) {
3318
5569
  if (ta && tb && ta !== tb) {
3319
5570
  return ta.localeCompare(tb);
3320
5571
  }
3321
- // 同一条 trace 内 → 按 messageIndex 派生的 order(跨 trace 比 order 没意义)
3322
- if (a.sourceTrace === b.sourceTrace) {
5572
+ // 同一条物理 trace 内 → 按 messageIndex 派生的 order(跨 trace 比 order 没意义)。
5573
+ const samePhysicalTrace = a.traceId && b.traceId
5574
+ ? a.traceId === b.traceId
5575
+ : a.sourceTrace === b.sourceTrace;
5576
+ if (samePhysicalTrace) {
3323
5577
  return a.order - b.order;
3324
5578
  }
3325
5579
  // 跨 trace 且 timestamp 不可比 → 主线优先(避免缺 timestamp 时 subagent 顶到最前)
@@ -3327,23 +5581,50 @@ function compareTimelineEvents(a, b) {
3327
5581
  const rankDiff = roleRank(a) - roleRank(b);
3328
5582
  if (rankDiff !== 0)
3329
5583
  return rankDiff;
3330
- // 同 traceRole 跨文件兜底:sourceTrace 字典序保证稳定
3331
- return a.sourceTrace.localeCompare(b.sourceTrace);
5584
+ // 同 traceRole 且 timestamp 缺失时,用物理 trace 身份提供稳定顺序。
5585
+ return (a.traceId ?? a.sourceTrace).localeCompare(b.traceId ?? b.sourceTrace)
5586
+ || a.sourceTrace.localeCompare(b.sourceTrace)
5587
+ || a.order - b.order;
5588
+ }
5589
+ function traceRecordRanges(events) {
5590
+ const byTrace = new Map();
5591
+ for (const event of events) {
5592
+ if (typeof event.messageIndex !== 'number')
5593
+ continue;
5594
+ const traceId = event.traceId ?? event.sourceTrace;
5595
+ const group = byTrace.get(traceId) ?? [];
5596
+ group.push(event);
5597
+ byTrace.set(traceId, group);
5598
+ }
5599
+ return Array.from(byTrace.entries())
5600
+ .map(([traceId, group]) => {
5601
+ const indexes = group.map((event) => event.messageIndex);
5602
+ return {
5603
+ traceId,
5604
+ sourceTrace: group[0].sourceTrace,
5605
+ startRecordIndex: Math.min(...indexes),
5606
+ endRecordIndex: Math.max(...indexes),
5607
+ eventCount: group.length,
5608
+ };
5609
+ })
5610
+ .sort((a, b) => a.traceId.localeCompare(b.traceId));
3332
5611
  }
3333
5612
  function buildSessionTimelineTree(sessionId, sessions) {
3334
- const mainSession = sessions.find((session) => session.traceRole === 'main')
3335
- ?? sessions.find((session) => session.traceRole === 'standalone');
3336
- const main = mainSession ? buildTimelineWindow(mainSession, 0, mainSession.records.length) : [];
5613
+ const mainSession = sessions.find((session) => session.role === 'main')
5614
+ ?? sessions.find((session) => session.role === 'standalone');
5615
+ const main = mainSession ? buildTimelineWindow(mainSession, 0, mainSession.events.length) : [];
3337
5616
  const branches = sessions
3338
5617
  .filter((session) => !mainSession || session !== mainSession)
3339
5618
  .map((session) => {
3340
- const events = buildTimelineWindow(session, 0, session.records.length);
5619
+ const events = buildTimelineWindow(session, 0, session.events.length);
3341
5620
  const attachTo = inferSubagentAttachment(main, session);
3342
5621
  return {
3343
- id: hashParts('timeline-branch', session.sourcePath),
3344
- label: session.traceLabel ?? session.sourcePath.split('/').pop() ?? 'subagent',
5622
+ id: hashParts('timeline-branch', session.traceId),
5623
+ label: (session.label ?? basename(session.sourcePath)) || 'subagent',
5624
+ sessionId: session.runId,
5625
+ traceId: session.traceId,
3345
5626
  sourceTrace: session.sourcePath,
3346
- traceRole: session.traceRole ?? 'subagent',
5627
+ traceRole: session.role,
3347
5628
  attachTo,
3348
5629
  events,
3349
5630
  };
@@ -3364,8 +5645,10 @@ function inferSubagentAttachment(mainEvents, branchSession) {
3364
5645
  if (!event)
3365
5646
  return undefined;
3366
5647
  return {
5648
+ traceId: event.traceId,
3367
5649
  sourceTrace: event.sourceTrace,
3368
5650
  messageIndex: event.messageIndex,
5651
+ callInstanceId: event.callInstanceId,
3369
5652
  toolUseId: event.toolUseId,
3370
5653
  label: event.toolName,
3371
5654
  };
@@ -3380,11 +5663,15 @@ function maxDefined(values) {
3380
5663
  }
3381
5664
  function minString(values) {
3382
5665
  const filtered = values.filter((value) => Boolean(value));
3383
- return filtered.length > 0 ? filtered.reduce((min, value) => value < min ? value : min, filtered[0]) : undefined;
5666
+ return filtered.length > 0
5667
+ ? filtered.reduce((min, value) => Date.parse(value) < Date.parse(min) ? value : min, filtered[0])
5668
+ : undefined;
3384
5669
  }
3385
5670
  function maxString(values) {
3386
5671
  const filtered = values.filter((value) => Boolean(value));
3387
- return filtered.length > 0 ? filtered.reduce((max, value) => value > max ? value : max, filtered[0]) : undefined;
5672
+ return filtered.length > 0
5673
+ ? filtered.reduce((max, value) => Date.parse(value) > Date.parse(max) ? value : max, filtered[0])
5674
+ : undefined;
3388
5675
  }
3389
5676
  function uniqueEvidenceRefs(refs) {
3390
5677
  const byId = new Map();
@@ -3397,57 +5684,6 @@ function inferUserGoal(userRefs) {
3397
5684
  const first = userRefs.find((ref) => ref.snippet && !ref.snippet.includes('tool_result'));
3398
5685
  return snippet(first?.snippet, 180);
3399
5686
  }
3400
- function isSkillContextRecord(record, text) {
3401
- const meta = record;
3402
- if (meta.isMeta === true && typeof meta.sourceToolUseID === 'string')
3403
- return true;
3404
- return /^Base directory for this skill:\s+.+(?:\n| )#\s+[a-z0-9][\w.-]*/i.test(text);
3405
- }
3406
- function isRuntimeContextRecord(record, text) {
3407
- const meta = record;
3408
- if (isRuntimeProtocolPromptText(text))
3409
- return true;
3410
- if (/^Conversation info \(untrusted metadata\):\s*```json/i.test(text))
3411
- return true;
3412
- if (meta.entrypoint !== 'sdk-ts' || typeof meta.promptId !== 'string')
3413
- return false;
3414
- return /^进入.+流程。当前页面已经完成本地工作区恢复/.test(text)
3415
- || /gui-workflow route/.test(text)
3416
- || /当前页面已经完成本地工作区恢复/.test(text);
3417
- }
3418
- function timestampOf(record) {
3419
- if (!record || typeof record !== 'object')
3420
- return undefined;
3421
- const value = record.timestamp;
3422
- return typeof value === 'string' ? value : undefined;
3423
- }
3424
- function sourceKindForPath(path) {
3425
- if (path.includes('/openclaw') || path.includes('/.openclaw/'))
3426
- return 'openclaw';
3427
- if (path.endsWith('.jsonl'))
3428
- return 'claude';
3429
- if (path.endsWith('.log'))
3430
- return 'markdown_log';
3431
- return 'unknown';
3432
- }
3433
- function inferEntrypointFromRecords(session) {
3434
- for (const record of session.records) {
3435
- if (!record || typeof record !== 'object')
3436
- continue;
3437
- const entrypoint = record.entrypoint;
3438
- if (typeof entrypoint === 'string' && entrypoint.trim())
3439
- return entrypoint;
3440
- }
3441
- if (session.entrypoint)
3442
- return session.entrypoint;
3443
- if (session.sourceKind === 'openclaw')
3444
- return 'openclaw';
3445
- if (session.sourceKind === 'markdown_log')
3446
- return 'markdown_log';
3447
- if (session.sourcePath.endsWith('.log'))
3448
- return 'markdown_log';
3449
- return undefined;
3450
- }
3451
5687
  function snippet(value, max = 240) {
3452
5688
  const text = typeof value === 'string' ? value : String(value ?? '');
3453
5689
  const normalized = text.replace(/\s+/g, ' ').trim();