oh-my-knowledge 0.48.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 (243) 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 +18 -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 +28 -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 +30 -2
  31. package/dist/cli/commands/sample.d.ts +6 -3
  32. package/dist/cli/commands/sample.js +72 -68
  33. package/dist/cli/lib/codex-model-hint.d.ts +9 -0
  34. package/dist/cli/lib/codex-model-hint.js +45 -0
  35. package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
  36. package/dist/cli/lib/generation-failure-hint.js +61 -0
  37. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  38. package/dist/cli/lib/i18n-dict/common.js +4 -0
  39. package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
  40. package/dist/cli/lib/i18n-dict/gen.js +38 -6
  41. package/dist/cli/lib/i18n-dict/help.js +6 -6
  42. package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
  43. package/dist/cli/lib/i18n-dict/init.js +13 -9
  44. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  45. package/dist/cli/lib/i18n-dict/run.js +34 -2
  46. package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
  47. package/dist/cli/lib/llm-failure-classifier.js +8 -0
  48. package/dist/cli/lib/parse-run-config.d.ts +6 -5
  49. package/dist/cli/lib/parse-run-config.js +16 -9
  50. package/dist/cli/lib/runtime-defaults.d.ts +21 -0
  51. package/dist/cli/lib/runtime-defaults.js +79 -0
  52. package/dist/diagnosis/observe-mapper.js +14 -15
  53. package/dist/diagnosis/observe-producer.js +3 -1
  54. package/dist/diagnosis/studio-projection.js +14 -7
  55. package/dist/diagnosis/types.d.ts +2 -0
  56. package/dist/diagnosis/types.js +12 -0
  57. package/dist/doctor/endpoint-rule.js +2 -1
  58. package/dist/eval-core/artifact-file-names.js +18 -1
  59. package/dist/eval-core/artifact-index.d.ts +7 -11
  60. package/dist/eval-core/artifact-index.js +139 -80
  61. package/dist/eval-core/cache.d.ts +12 -3
  62. package/dist/eval-core/cache.js +89 -29
  63. package/dist/eval-core/comparability.js +10 -6
  64. package/dist/eval-core/evaluation-execution.d.ts +2 -1
  65. package/dist/eval-core/evaluation-execution.js +122 -37
  66. package/dist/eval-core/evaluation-job.d.ts +4 -1
  67. package/dist/eval-core/evaluation-job.js +4 -1
  68. package/dist/eval-core/evaluation-reporting.d.ts +15 -13
  69. package/dist/eval-core/evaluation-reporting.js +54 -52
  70. package/dist/eval-core/execution-strategy.d.ts +2 -0
  71. package/dist/eval-core/execution-strategy.js +11 -9
  72. package/dist/eval-core/fact-checker.js +15 -7
  73. package/dist/eval-core/holdout.js +3 -2
  74. package/dist/eval-core/judge-independence.d.ts +2 -2
  75. package/dist/eval-core/mock-hook.cjs +23 -6
  76. package/dist/eval-core/mocks-runtime.js +30 -8
  77. package/dist/eval-core/report-document.d.ts +12 -0
  78. package/dist/eval-core/report-document.js +1151 -0
  79. package/dist/eval-core/report-extensions.d.ts +4 -0
  80. package/dist/eval-core/report-extensions.js +500 -0
  81. package/dist/eval-core/report-file-migration.js +7 -2
  82. package/dist/eval-core/resume-compatibility.d.ts +31 -0
  83. package/dist/eval-core/resume-compatibility.js +141 -0
  84. package/dist/eval-core/sample-fingerprint.d.ts +12 -0
  85. package/dist/eval-core/sample-fingerprint.js +193 -0
  86. package/dist/eval-core/schema.js +86 -31
  87. package/dist/eval-core/verdict.d.ts +8 -4
  88. package/dist/eval-core/verdict.js +24 -10
  89. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
  90. package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
  91. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
  92. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
  93. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
  94. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
  95. package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
  96. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
  97. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
  98. package/dist/eval-workflows/evaluation-pipeline.js +12 -7
  99. package/dist/eval-workflows/run-evaluation.d.ts +9 -7
  100. package/dist/eval-workflows/run-evaluation.js +79 -51
  101. package/dist/executors/anthropic-api.js +65 -9
  102. package/dist/executors/claude-cli.js +16 -79
  103. package/dist/executors/claude-protocol.d.ts +28 -0
  104. package/dist/executors/claude-protocol.js +180 -0
  105. package/dist/executors/claude-sdk-trace.js +56 -28
  106. package/dist/executors/claude-sdk.d.ts +1 -0
  107. package/dist/executors/claude-sdk.js +39 -93
  108. package/dist/executors/codex-cli-trace.js +166 -31
  109. package/dist/executors/codex-cli.d.ts +6 -8
  110. package/dist/executors/codex-cli.js +49 -151
  111. package/dist/executors/codex-protocol.d.ts +24 -0
  112. package/dist/executors/codex-protocol.js +234 -0
  113. package/dist/executors/codex-sdk.js +68 -120
  114. package/dist/executors/gemini.js +88 -13
  115. package/dist/executors/index.d.ts +2 -3
  116. package/dist/executors/index.js +5 -3
  117. package/dist/executors/openai-api.js +70 -9
  118. package/dist/executors/runtime-fingerprint.js +88 -11
  119. package/dist/executors/script-command.d.ts +8 -0
  120. package/dist/executors/script-command.js +87 -0
  121. package/dist/executors/script.js +202 -29
  122. package/dist/executors/shared.d.ts +35 -3
  123. package/dist/executors/shared.js +113 -15
  124. package/dist/grading/assertions.d.ts +1 -1
  125. package/dist/grading/assertions.js +19 -9
  126. package/dist/grading/diagnostic.d.ts +9 -2
  127. package/dist/grading/diagnostic.js +25 -2
  128. package/dist/grading/index.js +10 -4
  129. package/dist/grading/judge.js +19 -6
  130. package/dist/grading/layered-scores.d.ts +2 -3
  131. package/dist/grading/layered-scores.js +2 -3
  132. package/dist/inputs/load-samples.d.ts +1 -2
  133. package/dist/inputs/load-samples.js +23 -1
  134. package/dist/inputs/mcp-resolver.js +6 -3
  135. package/dist/inputs/sample-document.d.ts +11 -0
  136. package/dist/inputs/sample-document.js +96 -0
  137. package/dist/managed/evidence.d.ts +1 -0
  138. package/dist/managed/evidence.js +1 -1
  139. package/dist/managed/store.js +200 -91
  140. package/dist/observability/codex-trace-adapter.d.ts +5 -0
  141. package/dist/observability/codex-trace-adapter.js +850 -0
  142. package/dist/observability/experience.d.ts +32 -6
  143. package/dist/observability/experience.js +2695 -459
  144. package/dist/observability/feedback-matchers.js +16 -1
  145. package/dist/observability/inbox-view-model.d.ts +1 -1
  146. package/dist/observability/inbox-view-model.js +19 -14
  147. package/dist/observability/inbox.d.ts +7 -1
  148. package/dist/observability/inbox.js +632 -124
  149. package/dist/observability/problem-patterns.js +2 -0
  150. package/dist/observability/review-state.d.ts +6 -0
  151. package/dist/observability/review-state.js +235 -63
  152. package/dist/observability/skill-chain-advisories.js +1 -1
  153. package/dist/observability/skill-chain.js +17 -4
  154. package/dist/observability/skill-health-analyzer.d.ts +32 -7
  155. package/dist/observability/skill-health-analyzer.js +194 -121
  156. package/dist/observability/skill-health-report.d.ts +10 -0
  157. package/dist/observability/skill-health-report.js +620 -0
  158. package/dist/observability/soft-standards/constants.d.ts +0 -1
  159. package/dist/observability/soft-standards/constants.js +0 -1
  160. package/dist/observability/soft-standards/index.d.ts +1 -1
  161. package/dist/observability/soft-standards/index.js +1 -1
  162. package/dist/observability/soft-standards/llm-extractor.js +8 -10
  163. package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
  164. package/dist/observability/soft-standards/skill-standards-store.js +59 -18
  165. package/dist/observability/soft-standards/types.d.ts +2 -2
  166. package/dist/observability/trace-adapter.d.ts +12 -7
  167. package/dist/observability/trace-adapter.js +11 -9
  168. package/dist/observability/trace-attribution.d.ts +13 -5
  169. package/dist/observability/trace-attribution.js +315 -21
  170. package/dist/observability/trace-ingestion.d.ts +9 -0
  171. package/dist/observability/trace-ingestion.js +80 -0
  172. package/dist/observability/trace-ir.d.ts +113 -0
  173. package/dist/observability/trace-ir.js +87 -0
  174. package/dist/observability/trace-segmenter.d.ts +19 -6
  175. package/dist/observability/trace-segmenter.js +377 -196
  176. package/dist/observability/trace-session-index.d.ts +19 -0
  177. package/dist/observability/trace-session-index.js +68 -0
  178. package/dist/observability/trace-source.d.ts +12 -4
  179. package/dist/observability/trace-source.js +939 -215
  180. package/dist/renderer/html-renderer.js +37 -6
  181. package/dist/renderer/icons.js +3 -0
  182. package/dist/renderer/observation-inbox-renderer.js +208 -90
  183. package/dist/renderer/skill-detail-renderer.js +452 -109
  184. package/dist/renderer/skill-health-renderer.js +69 -12
  185. package/dist/renderer/summary.js +28 -7
  186. package/dist/renderer/table.js +21 -4
  187. package/dist/renderer/test-view.d.ts +1 -0
  188. package/dist/renderer/test-view.js +44 -9
  189. package/dist/server/indexed-report-store.js +14 -18
  190. package/dist/server/job-store.js +64 -26
  191. package/dist/server/report-server.js +190 -78
  192. package/dist/server/report-store.js +57 -80
  193. package/dist/server/skill-index.js +143 -49
  194. package/dist/server/skill-insights.js +44 -5
  195. package/dist/shared/artifact-graph.d.ts +3 -0
  196. package/dist/shared/artifact-graph.js +224 -0
  197. package/dist/shared/assertion-types.d.ts +8 -0
  198. package/dist/shared/assertion-types.js +46 -0
  199. package/dist/shared/atomic-json.d.ts +8 -0
  200. package/dist/shared/atomic-json.js +33 -0
  201. package/dist/shared/diagnosis-schema.d.ts +9 -0
  202. package/dist/shared/diagnosis-schema.js +181 -0
  203. package/dist/shared/doctor-report.d.ts +3 -0
  204. package/dist/shared/doctor-report.js +103 -0
  205. package/dist/shared/evaluation-job.d.ts +6 -0
  206. package/dist/shared/evaluation-job.js +217 -0
  207. package/dist/shared/executor-result.d.ts +17 -0
  208. package/dist/shared/executor-result.js +221 -0
  209. package/dist/shared/file-lock.d.ts +12 -0
  210. package/dist/shared/file-lock.js +129 -0
  211. package/dist/shared/json-value.d.ts +5 -0
  212. package/dist/shared/json-value.js +36 -0
  213. package/dist/shared/keyed-mutex.d.ts +7 -0
  214. package/dist/shared/keyed-mutex.js +24 -0
  215. package/dist/shared/record-count.d.ts +8 -0
  216. package/dist/shared/record-count.js +43 -0
  217. package/dist/shared/sample-contract.d.ts +3 -0
  218. package/dist/shared/sample-contract.js +332 -0
  219. package/dist/shared/timestamp.d.ts +6 -0
  220. package/dist/shared/timestamp.js +64 -0
  221. package/dist/shared/token-usage.d.ts +19 -0
  222. package/dist/shared/token-usage.js +50 -0
  223. package/dist/shared/tool-call-status.d.ts +8 -0
  224. package/dist/shared/tool-call-status.js +28 -0
  225. package/dist/shared/tool-identity.d.ts +21 -0
  226. package/dist/shared/tool-identity.js +84 -0
  227. package/dist/shared/tool-search.js +73 -16
  228. package/dist/shared/trace-projection.d.ts +5 -0
  229. package/dist/shared/trace-projection.js +20 -0
  230. package/dist/shared/trace-source-kind.d.ts +3 -0
  231. package/dist/shared/trace-source-kind.js +12 -0
  232. package/dist/types/diagnosis.d.ts +2 -0
  233. package/dist/types/eval.d.ts +4 -0
  234. package/dist/types/executor.d.ts +32 -5
  235. package/dist/types/index.d.ts +1 -0
  236. package/dist/types/index.js +1 -0
  237. package/dist/types/judge.d.ts +2 -0
  238. package/dist/types/observability.d.ts +116 -9
  239. package/dist/types/report.d.ts +58 -6
  240. package/dist/types/skill-index.d.ts +7 -0
  241. package/dist/types/trace.d.ts +2 -0
  242. package/dist/types/trace.js +1 -0
  243. package/package.json +9 -5
@@ -1,7 +1,8 @@
1
1
  import { DEFAULT_LANG, layout, e } from './layout.js';
2
- import { severityReasonFor, observationMetricAnnotationTargetId, getSkillChainAdvisory, resolveAdvisoryCommand, ASSISTANT_DELIVERABLE_ARTIFACT_RE, hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, HARD_RULE_TEXT_RE, isAssistantProgressUpdateText, isScheduledTaskPromptText, isSyntheticUserMessageText, isUserInteractionMetricText, } from '../observability/inbox-view-model.js';
2
+ import { severityReasonFor, observationMetricAnnotationEntry, observationMetricAnnotationTargetId, getSkillChainAdvisory, resolveAdvisoryCommand, ASSISTANT_DELIVERABLE_ARTIFACT_RE, hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, HARD_RULE_TEXT_RE, isAssistantProgressUpdateText, isScheduledTaskPromptText, isSyntheticUserMessageText, isUserInteractionMetricText, } from '../observability/inbox-view-model.js';
3
3
  import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasUserCorrectionSignal, hasUserGoalShiftSignal } from '../observability/feedback-projection.js';
4
4
  import { durationMsBetween } from '../shared/time.js';
5
+ import { incrementRecordCount } from '../shared/record-count.js';
5
6
  import { OBSERVATION_INBOX_STYLES } from './observation-inbox/styles.js';
6
7
  import { experienceSkillAnchor, formatTimeRange as formatTimeRangeImpl, formatTimestamp, renderArtifactVersion, renderField, renderJson, renderFeedbackAttributionLabel, skillAnchor, truncateText, } from './observation-inbox/helpers.js';
7
8
  import { shellQuoteArg } from '../shared/shell-quote.js';
@@ -17,18 +18,18 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
17
18
  for (const invocation of experience?.invocations ?? []) {
18
19
  const toolCounts = experienceToolCountsBySkill.get(invocation.skillName) ?? {};
19
20
  for (const [tool, count] of Object.entries(invocation.toolCounts ?? {})) {
20
- toolCounts[tool] = (toolCounts[tool] ?? 0) + count;
21
+ incrementRecordCount(toolCounts, tool, count);
21
22
  }
22
23
  experienceToolCountsBySkill.set(invocation.skillName, toolCounts);
23
24
  const entrypoint = invocation.entrypoint ?? invocation.sourceKind ?? 'unknown';
24
25
  const entrypointCounts = experienceEntrypointCountsBySkill.get(invocation.skillName) ?? {};
25
- entrypointCounts[entrypoint] = (entrypointCounts[entrypoint] ?? 0) + 1;
26
+ incrementRecordCount(entrypointCounts, entrypoint);
26
27
  experienceEntrypointCountsBySkill.set(invocation.skillName, entrypointCounts);
27
28
  const originLabel = invocation.attribution.pluginName
28
29
  ? `Skill 来源:插件 ${invocation.attribution.pluginName}`
29
30
  : 'Skill 来源:本地 skill';
30
31
  const originCounts = experienceSkillOriginCountsBySkill.get(invocation.skillName) ?? {};
31
- originCounts[originLabel] = (originCounts[originLabel] ?? 0) + 1;
32
+ incrementRecordCount(originCounts, originLabel);
32
33
  experienceSkillOriginCountsBySkill.set(invocation.skillName, originCounts);
33
34
  const attributionMethod = invocation.attribution.source === 'command-name'
34
35
  ? '通过斜杠命令触发'
@@ -44,7 +45,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
44
45
  const attributionTarget = invocation.attribution.commandName ?? invocation.attribution.rawSkillRef ?? invocation.skillName;
45
46
  const attributionLabel = attributionTarget ? `${attributionMethod}:${attributionTarget}` : attributionMethod;
46
47
  const attributionCounts = experienceAttributionCountsBySkill.get(invocation.skillName) ?? {};
47
- attributionCounts[attributionLabel] = (attributionCounts[attributionLabel] ?? 0) + 1;
48
+ incrementRecordCount(attributionCounts, attributionLabel);
48
49
  experienceAttributionCountsBySkill.set(invocation.skillName, attributionCounts);
49
50
  }
50
51
  const countSkillsBySeverity = (...severities) => new Set(allItems.filter((item) => severities.includes(item.severity)).map((item) => item.skillName)).size;
@@ -53,6 +54,27 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
53
54
  const latestInvocationLabel = lang === 'zh' ? '最近调用' : 'Latest invocation';
54
55
  const invocationWindowLabel = lang === 'zh' ? '调用窗口' : 'Invocation window';
55
56
  const formatTimeRange = (start, end, durationMs) => formatTimeRangeImpl(start, end, durationMs, lang);
57
+ const timestampedOccurrences = (item) => item.timestampedOccurrences
58
+ ?? (item.firstSeen === '1970-01-01T00:00:00.000Z' ? 0 : item.occurrences);
59
+ const observedItemTimestamp = (item, value) => timestampedOccurrences(item) > 0
60
+ ? value.slice(0, 19).replace('T', ' ')
61
+ : (lang === 'zh' ? '未记录' : 'Not recorded');
62
+ const sessionTimestampedInvocationCount = (session) => session.timestampedInvocationCount
63
+ ?? (session.startTimestamp === '1970-01-01T00:00:00.000Z' ? 0 : session.invocationIds.length);
64
+ const observedSessionRange = (session) => {
65
+ const count = sessionTimestampedInvocationCount(session);
66
+ if (count === 0)
67
+ return lang === 'zh' ? '未记录' : 'Not recorded';
68
+ const range = formatTimeRange(session.startTimestamp, session.endTimestamp);
69
+ return count < session.invocationIds.length
70
+ ? `${range}${lang === 'zh' ? '(部分调用无时间)' : ' (partial timestamps)'}`
71
+ : range;
72
+ };
73
+ const observedSessionTimestamp = (session, value) => sessionTimestampedInvocationCount(session) > 0
74
+ ? value.slice(0, 19).replace('T', ' ')
75
+ : (lang === 'zh' ? '未记录' : 'Not recorded');
76
+ const skillTimestampedInvocationCount = (skill) => skill.timestampedInvocationCount
77
+ ?? (skill.firstSeen === '1970-01-01T00:00:00.000Z' ? 0 : skill.invocationCount);
56
78
  const reviewSeverityMeta = (item) => {
57
79
  if (item.severity === 'high') {
58
80
  return { label: '高风险/需关注', decision: '优先看,可能要补 SKILL.md 或改 skill 说明', color: 'var(--red)', bg: 'rgba(220,38,38,.08)' };
@@ -166,8 +188,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
166
188
  </div>`;
167
189
  };
168
190
  const renderSourceBadge = (item) => {
169
- const label = item.sourceKind === 'openclaw' ? 'OpenClaw' : item.sourceKind === 'markdown_log' ? 'Markdown log' : item.sourceKind === 'claude' ? 'Claude' : 'Unknown';
170
- const color = item.sourceKind === 'openclaw' ? '#7c3aed' : item.sourceKind === 'markdown_log' ? 'var(--green)' : item.sourceKind === 'claude' ? 'var(--accent)' : 'var(--text-muted)';
191
+ const label = item.sourceKind === 'openclaw' ? 'OpenClaw' : item.sourceKind === 'codex' ? 'Codex' : item.sourceKind === 'markdown_log' ? 'Markdown log' : item.sourceKind === 'claude' ? 'Claude' : 'Unknown';
192
+ const color = item.sourceKind === 'openclaw' ? '#7c3aed' : item.sourceKind === 'codex' ? '#1677ff' : item.sourceKind === 'markdown_log' ? 'var(--green)' : item.sourceKind === 'claude' ? 'var(--accent)' : 'var(--text-muted)';
171
193
  return `<span title="调用日志来源:${e(label)}" style="display:inline-flex;margin-top:4px;padding:2px 6px;border-radius:999px;background:var(--bg-muted);color:${color};font-size:11px;font-weight:650">${e(label)}</span>`;
172
194
  };
173
195
  const confidenceHeaderHelp = '判断把握:OMK 对“这条 过程发现 是否需要处理/是否高风险/需关注”的规则判断有多确定。归属把握:OMK 把这条 过程发现 归到当前 skill 名下有多确定,例如明确调用 skill 通常更高。';
@@ -183,6 +205,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
183
205
  repeatedExecution: '重复执行',
184
206
  toolCall: '工具调用',
185
207
  toolFailure: '工具执行失败',
208
+ toolCancelled: '工具调用取消',
209
+ toolUnknown: '工具状态未知',
186
210
  highObservation: '高优先级过程发现',
187
211
  mediumObservation: '低风险/抽样过程发现',
188
212
  hedging: '不确定表达',
@@ -216,6 +240,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
216
240
  repeatedExecution: '统计同类步骤、工具或流程被重复执行的信号。高频出现通常对应绕路、工具策略不清晰或 workflow 缺少明确顺序。',
217
241
  toolCall: '统计该 skill 运行片段里的 tool_use 调用总数,包括 Bash、Read、Grep 等工具。',
218
242
  toolFailure: '统计该 skill 运行片段里失败的工具执行结果,例如 tool_result 标记 is_error=true。注意:工具执行失败不等于整个 skill 调用失败。',
243
+ toolCancelled: '统计 runtime 明确标记为 cancelled 的工具调用。取消与工具执行失败分开统计,也不作为知识缺口证据。',
244
+ toolUnknown: '统计 runtime 未提供可信终态的工具调用。状态未知不计入工具成功率或失败率分母。',
219
245
  highObservation: '统计 severity=high 的过程发现,通常表示可能需要优先复盘的执行问题。',
220
246
  mediumObservation: '统计 severity=medium/low 的过程发现,通常进入抽样复盘,不直接等同于必须改 skill。',
221
247
  hedging: '统计回答或过程发现里的“不确定 / 可能 / 需要确认”等低置信文本信号。',
@@ -243,6 +269,11 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
243
269
  'claude-desktop': 'Claude Code App',
244
270
  cli: 'Claude CLI',
245
271
  'sdk-ts': 'Claude SDK',
272
+ 'codex-desktop': 'ChatGPT 桌面端(Codex)',
273
+ 'codex-cli': 'Codex CLI',
274
+ 'codex-sdk': 'Codex SDK',
275
+ 'codex-vscode': 'Codex VS Code',
276
+ claudian: 'Claudian',
246
277
  openclaw: 'OpenClaw',
247
278
  markdown_log: 'Markdown 日志',
248
279
  };
@@ -266,15 +297,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
266
297
  const renderInvocationSummary = (indicators, invocationCount) => {
267
298
  const total = indicators.toolCallCount;
268
299
  const failed = Math.max(0, indicators.toolFailureCount);
300
+ const cancelled = Math.max(0, indicators.toolCancelledCount ?? 0);
301
+ const unknown = Math.max(0, indicators.toolUnknownCount ?? 0);
269
302
  const interrupted = Math.max(0, indicators.userInterruptionCount);
270
- const success = Math.max(0, total - failed);
303
+ const success = Math.max(0, total - failed - cancelled - unknown);
271
304
  const pct = (value) => total > 0 ? `${Math.round(value / total * 100)}%` : '—';
272
305
  return `<div class="invocation-summary" title="这是 trace 中可观测到的调用过程汇总。工具执行失败/人工中断是过程信号,不直接等同于整个 skill 失败或用户目标失败。">
273
306
  <div class="invocation-total">工具调用总次数 <strong>${total}</strong></div>
274
307
  <div class="invocation-breakdown">
275
308
  <span>工具执行成功 ${success} / ${pct(success)}</span>
276
309
  <span>工具执行失败 ${failed} / ${pct(failed)}</span>
277
- <span>人工中断 ${interrupted} / ${pct(interrupted)}</span>
310
+ ${cancelled > 0 ? `<span>工具调用取消 ${cancelled} / ${pct(cancelled)}</span>` : ''}
311
+ ${unknown > 0 ? `<span>结果状态未知 ${unknown} / ${pct(unknown)}</span>` : ''}
312
+ <span>人工中断 ${interrupted} 次</span>
278
313
  </div>
279
314
  <div class="invocation-footnote">Skill 调用段 ${invocationCount}</div>
280
315
  </div>`;
@@ -343,7 +378,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
343
378
  const next = {};
344
379
  for (const [key, count] of Object.entries(counts ?? {})) {
345
380
  const display = label(key);
346
- next[display] = (next[display] ?? 0) + count;
381
+ incrementRecordCount(next, display, count);
347
382
  }
348
383
  return next;
349
384
  };
@@ -411,7 +446,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
411
446
  if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
412
447
  for (const [key, value] of Object.entries(legacy)) {
413
448
  if (typeof value === 'number')
414
- merged[key] = (merged[key] ?? 0) + value;
449
+ incrementRecordCount(merged, key, value);
415
450
  }
416
451
  }
417
452
  return merged;
@@ -505,6 +540,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
505
540
  repeatedExecutionCount: 0,
506
541
  toolCallCount: 0,
507
542
  toolFailureCount: 0,
543
+ toolCancelledCount: 0,
544
+ toolUnknownCount: 0,
508
545
  highObservationCount: 0,
509
546
  mediumObservationCount: 0,
510
547
  hedgingCount: 0,
@@ -516,8 +553,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
516
553
  return pattern.test(value);
517
554
  };
518
555
  const displayMetricVerdict = (event, metricKey, metricScopeId) => {
519
- const targetId = observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
520
- const entry = reviewState.entries[`evidence_metric:${targetId}`];
556
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
521
557
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
522
558
  };
523
559
  const displayMetricIsActive = (event, metricKey, ruleDetected, metricScopeId) => {
@@ -720,6 +756,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
720
756
  repeatedExecutionCount: current.repeatedExecutionCount + (next.repeatedExecutionCount ?? 0),
721
757
  toolCallCount: current.toolCallCount + next.toolCallCount,
722
758
  toolFailureCount: current.toolFailureCount + next.toolFailureCount,
759
+ toolCancelledCount: (current.toolCancelledCount ?? 0) + (next.toolCancelledCount ?? 0),
760
+ toolUnknownCount: (current.toolUnknownCount ?? 0) + (next.toolUnknownCount ?? 0),
723
761
  highObservationCount: current.highObservationCount + next.highObservationCount,
724
762
  mediumObservationCount: current.mediumObservationCount + next.mediumObservationCount,
725
763
  hedgingCount: current.hedgingCount + next.hedgingCount,
@@ -870,8 +908,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
870
908
  const displayToolCounts = mergeCountsByLabel(toolCounts, userFacingToolLabel);
871
909
  const total = indicators.toolCallCount;
872
910
  const failed = Math.max(0, indicators.toolFailureCount);
911
+ const cancelled = Math.max(0, indicators.toolCancelledCount ?? 0);
912
+ const unknown = Math.max(0, indicators.toolUnknownCount ?? 0);
873
913
  const interrupted = Math.max(0, indicators.userInterruptionCount);
874
- const success = Math.max(0, total - failed);
914
+ const success = Math.max(0, total - failed - cancelled - unknown);
875
915
  const processMetrics = [
876
916
  renderDecisionMetricIfPositive('高优先级', indicators.highObservationCount, 'priority'),
877
917
  renderDecisionMetricIfPositive('抽样过程发现', indicators.mediumObservationCount, 'sample'),
@@ -882,9 +922,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
882
922
  renderDecisionMetricIfPositive('显式缺口', indicators.explicitMarkerCount, 'sample'),
883
923
  ].filter(Boolean).join('');
884
924
  return `<div class="skill-evidence-summary">
885
- <div class="summary-row"><span class="summary-title">【skill 运行】</span>${renderMetric('调用段', skill.invocationCount, '次')}<span class="summary-muted">分布 ${skill.sessionCount} 个 session</span>${renderMetric('工具调用', total, '次')}${renderMetricShare('成功', success, total)}${renderDecisionMetricShare('失败', failed, total, 'sample')}${renderDecisionMetricShare('人工中断', interrupted, total, 'priority')}${renderDecisionMetricIfPositive('自我纠正', indicators.selfCorrectionCount ?? 0, 'sample')}${renderDecisionMetricIfPositive('重复执行', indicators.repeatedExecutionCount ?? 0, 'sample')}${renderSoftMetric('有结果', indicators.assistantDeliverySignalCount ?? 0, '助手回复里出现明确完成态或结果反馈。')}${renderSoftMetric('有产物', indicators.deliverableArtifactSignalCount ?? 0, '助手回复里出现文档链接、Demo 地址、文件路径、代码块或上传产物。')}</div>
925
+ <div class="summary-row"><span class="summary-title">【skill 运行】</span>${renderMetric('调用段', skill.invocationCount, '次')}<span class="summary-muted">分布 ${skill.sessionCount} 个 session</span>${renderMetric('工具调用', total, '次')}${renderMetricShare('成功', success, total)}${renderDecisionMetricShare('失败', failed, total, 'sample')}${cancelled > 0 ? renderMetricShare('取消', cancelled, total) : ''}${unknown > 0 ? renderMetricShare('状态未知', unknown, total) : ''}${renderDecisionMetric('人工中断', interrupted, 'priority')}${renderDecisionMetricIfPositive('自我纠正', indicators.selfCorrectionCount ?? 0, 'sample')}${renderDecisionMetricIfPositive('重复执行', indicators.repeatedExecutionCount ?? 0, 'sample')}${renderSoftMetric('有结果', indicators.assistantDeliverySignalCount ?? 0, '助手回复里出现明确完成态或结果反馈。')}${renderSoftMetric('有产物', indicators.deliverableArtifactSignalCount ?? 0, '助手回复里出现文档链接、Demo 地址、文件路径、代码块或上传产物。')}</div>
886
926
  <div class="summary-row"><span class="summary-title">【用户交互】</span>${renderDecisionMetric('用户纠正', indicators.userCorrectionCount, 'priority')}${renderDecisionMetric('人工中断', indicators.userInterruptionCount, 'priority')}${renderMetric('追问', indicators.userFollowUpCount)}${renderDecisionMetric('负向反馈', indicators.negativeFeedbackCount ?? 0, 'priority')}${renderMetric('正向反馈', indicators.positiveFeedbackCount ?? 0)}${renderMetric('目标切换', indicators.userGoalShiftCount ?? 0)}</div>
887
- <div class="summary-row"><span class="summary-title">【工具调用】</span>${renderMetric('总计', total, '次')}<span class="summary-detail">(${renderRankedCounts(displayToolCounts, total)})</span>${renderDecisionMetricShare('工具执行失败', failed, total, 'sample')}</div>
927
+ <div class="summary-row"><span class="summary-title">【工具调用】</span>${renderMetric('总计', total, '次')}<span class="summary-detail">(${renderRankedCounts(displayToolCounts, total)})</span>${renderDecisionMetricShare('工具执行失败', failed, total, 'sample')}${cancelled > 0 ? renderMetricShare('工具调用取消', cancelled, total) : ''}${unknown > 0 ? renderMetricShare('结果状态未知', unknown, total) : ''}</div>
888
928
  <div class="summary-row"><span class="summary-title">【过程发现】</span>${processMetrics || '<span class="summary-muted">未发现需要展示的过程信号</span>'}</div>
889
929
  ${renderSkillProblemPatterns(skill.skillName, skill.problemPatterns)}
890
930
  </div>`;
@@ -1856,6 +1896,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
1856
1896
  };
1857
1897
  const metrics = report.oneLookMetrics;
1858
1898
  const tokenUsage = metrics.tokenUsage;
1899
+ const tokenCoverage = tokenUsage.coverage ?? 0;
1900
+ const tokenUsageText = tokenCoverage > 0
1901
+ ? `输入 ${tokenUsage.inputTokens} / 输出 ${tokenUsage.outputTokens} / 缓存读取 ${tokenUsage.cacheReadTokens} / 缓存写入 ${tokenUsage.cacheCreationTokens}${tokenCoverage < 1 ? `(覆盖 ${Math.round(tokenCoverage * 100)}% 调用)` : ''}`
1902
+ : '—(trace 未提供可归因的 token 用量)';
1859
1903
  const traceLinks = report.traceLinks.slice(0, 8);
1860
1904
  const story = report.sessionStory ?? fallbackSessionStory(report);
1861
1905
  const storyEvidenceButtons = (refs) => refs.length > 0
@@ -2149,6 +2193,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2149
2193
  ${[
2150
2194
  ['工具调用', metrics.toolCallCount],
2151
2195
  ['工具失败', metrics.toolFailureCount],
2196
+ ['工具取消', metrics.toolCancelledCount ?? 0],
2197
+ ['状态未知', metrics.toolUnknownCount ?? 0],
2152
2198
  ['用户消息', metrics.userMessageCount],
2153
2199
  ['追问/补充', metrics.userFollowUpCount],
2154
2200
  ['有结果', metrics.assistantDeliverySignalCount],
@@ -2159,7 +2205,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2159
2205
  ].map(([label, value]) => `<div style="padding:7px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface)"><div style="font-size:10px;color:var(--text-muted)">${e(String(label))}</div><strong style="font-size:13px;color:var(--text-primary)">${e(String(value))}</strong></div>`).join('')}
2160
2206
  </div>
2161
2207
  <div style="font-size:11px;color:var(--text-muted);line-height:1.5;margin-bottom:8px">
2162
- Token 使用量(按本次能力片段归因):输入 ${tokenUsage.inputTokens} / 输出 ${tokenUsage.outputTokens} / 缓存读取 ${tokenUsage.cacheReadTokens} / 缓存写入 ${tokenUsage.cacheCreationTokens}
2208
+ Token 使用量(按本次能力片段归因):${tokenUsageText}
2163
2209
  </div>
2164
2210
  <div style="font-size:12px;font-weight:700;color:var(--text-primary);margin-bottom:6px">证据定位</div>
2165
2211
  <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
@@ -2335,8 +2381,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2335
2381
  if (event.kind === 'tool_use')
2336
2382
  return { icon: 'TU', label: '工具调用', tone: 'tool-use' };
2337
2383
  if (event.kind === 'tool_result') {
2338
- if (event.isError)
2384
+ if (event.toolStatus === 'failure' || (event.toolStatus === undefined && event.isError))
2339
2385
  return { icon: 'TR', label: '工具执行失败', tone: 'tool-error' };
2386
+ if (event.toolStatus === 'cancelled')
2387
+ return { icon: 'TR', label: '工具调用取消', tone: 'runtime' };
2388
+ if (event.toolStatus === 'unknown')
2389
+ return { icon: 'TR', label: '工具状态未知', tone: 'runtime' };
2340
2390
  return { icon: 'TR', label: isSkillLaunchResult(event) ? '工具调用成功' : '工具执行成功', tone: 'tool-result' };
2341
2391
  }
2342
2392
  if (event.kind === 'skill_context')
@@ -2378,8 +2428,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2378
2428
  return /重复(?:执行|尝试|读取|搜索|调用)|再次(?:执行|读取|搜索|调用)|重新(?:执行|读取|搜索|调用|跑|运行)|再(?:执行|读取|搜索|调用|跑)一遍|重试|\b(?:retry|rerun)\b/i.test(text);
2379
2429
  }
2380
2430
  const evidenceMetricVerdictFor = (event, metricKey, metricScopeId) => {
2381
- const targetId = observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
2382
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2431
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2383
2432
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
2384
2433
  };
2385
2434
  const evidenceMetricIsActive = (event, metricKey, ruleDetected, metricScopeId) => {
@@ -2558,9 +2607,15 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2558
2607
  if (event.kind === 'tool_result') {
2559
2608
  if (evidenceMetricIsActive(event, 'repeated_execution', hasRepeatedExecutionSignal(event)))
2560
2609
  badges.push({ label: evidenceMetricBadgeLabel('重复执行', event, 'repeated_execution', hasRepeatedExecutionSignal(event)), className: 'metric-repeated-execution', title: '同类工具结果或失败恢复路径被重复出现。高频出现时通常对应绕路或 workflow 不清晰。' });
2561
- if (event.isError) {
2610
+ if (event.toolStatus === 'failure' || (event.toolStatus === undefined && event.isError)) {
2562
2611
  badges.push({ label: '工具执行失败', className: 'metric-tool-failure', title: 'tool_result 标记 is_error=true,表示这次工具执行失败。' });
2563
2612
  }
2613
+ else if (event.toolStatus === 'cancelled') {
2614
+ badges.push({ label: '工具调用取消', className: 'metric-neutral', title: 'runtime 明确标记这次工具调用已取消;它不计作工具执行失败。' });
2615
+ }
2616
+ else if (event.toolStatus === 'unknown') {
2617
+ badges.push({ label: '工具状态未知', className: 'metric-neutral', title: 'runtime 没有提供可确认的成功或失败状态;这次调用不计入工具成功率分母。' });
2618
+ }
2564
2619
  else if (isSkillLaunchResult(event)) {
2565
2620
  badges.push({ label: '工具调用成功', className: 'metric-tool-success', title: 'Launching skill 表示 Skill 工具调用已被 runtime 接受并启动;这不是 skill 执行结束。' });
2566
2621
  }
@@ -2650,13 +2705,11 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2650
2705
  };
2651
2706
  const metricAnnotationTarget = (event, metricKey, metricScopeId) => observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
2652
2707
  const metricAnnotationVerdict = (event, metricKey, metricScopeId) => {
2653
- const targetId = metricAnnotationTarget(event, metricKey, metricScopeId);
2654
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2708
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2655
2709
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
2656
2710
  };
2657
2711
  const metricAnnotationReason = (event, metricKey, metricScopeId) => {
2658
- const targetId = metricAnnotationTarget(event, metricKey, metricScopeId);
2659
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2712
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2660
2713
  return typeof entry?.reason === 'string' ? entry.reason : '';
2661
2714
  };
2662
2715
  const timelineManualMetricKeys = () => [
@@ -2692,10 +2745,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2692
2745
  ? '已加入窗口'
2693
2746
  : '加入窗口';
2694
2747
  const source = {
2748
+ traceId: event.traceId,
2695
2749
  sourceTrace: event.sourceTrace,
2696
2750
  sessionId: event.sessionId,
2697
2751
  messageIndex: event.messageIndex,
2698
2752
  messageUuid: event.messageUuid,
2753
+ callInstanceId: event.callInstanceId,
2699
2754
  toolUseId: event.toolUseId,
2700
2755
  snippet: (event.snippet ?? '').slice(0, 240),
2701
2756
  };
@@ -2707,10 +2762,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2707
2762
  data-manual-mark-goal-action="${e(goalAction)}"
2708
2763
  data-manual-mark-metrics="${e(JSON.stringify(metrics))}"
2709
2764
  data-manual-mark-source="${e(JSON.stringify(source))}"
2765
+ data-trace-id="${e(event.traceId ?? '')}"
2710
2766
  data-source-trace="${e(event.sourceTrace)}"
2711
2767
  data-session-id="${e(event.sessionId)}"
2712
2768
  data-message-index="${event.messageIndex ?? ''}"
2713
2769
  data-message-uuid="${e(event.messageUuid ?? '')}"
2770
+ data-call-instance-id="${e(event.callInstanceId ?? '')}"
2714
2771
  data-tool-use-id="${e(event.toolUseId ?? '')}"
2715
2772
  data-snippet="${e((event.snippet ?? '').slice(0, 240))}"
2716
2773
  onclick="openTimelineManualMark(this)"
@@ -2733,16 +2790,6 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2733
2790
  const isInsideSkillWindow = (event) => {
2734
2791
  if (options.activeEventIds)
2735
2792
  return options.activeEventIds.has(event.id);
2736
- if (options.activeSourceTrace && event.sourceTrace !== options.activeSourceTrace)
2737
- return false;
2738
- if (typeof event.messageIndex !== 'number')
2739
- return true;
2740
- const start = options.activeStartRecordIndex;
2741
- const end = options.activeEndRecordIndex;
2742
- if (typeof start === 'number' && event.messageIndex < start)
2743
- return false;
2744
- if (typeof end === 'number' && event.messageIndex > end)
2745
- return false;
2746
2793
  return true;
2747
2794
  };
2748
2795
  const isAddedToSkillWindow = (event) => goalSliceCorrectionAction(reviewSessionId, event) === 'add_to_current_skill_window';
@@ -2881,16 +2928,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2881
2928
  </div>`;
2882
2929
  };
2883
2930
  const renderSessionTimelineTree = (session) => {
2884
- const activeEventIds = new Set(session.timelinePreview.map((event) => event.id));
2931
+ const sessionInvocationIds = new Set(session.invocationIds);
2932
+ const invocationEventIds = (experience?.invocations ?? [])
2933
+ .filter((invocation) => sessionInvocationIds.has(invocation.id))
2934
+ .flatMap((invocation) => invocation.timelineEventIds ?? invocation.timeline.map((event) => event.id));
2935
+ const activeEventIds = new Set(invocationEventIds.length > 0
2936
+ ? invocationEventIds
2937
+ : session.timelinePreview.map((event) => event.id));
2885
2938
  const fullTimelineOptions = {
2886
2939
  reviewSessionId: session.id,
2887
2940
  currentSkillName: session.skillName,
2888
2941
  includeDownstreamFeedback: shouldIncludeDownstreamFeedbackForDisplay(session),
2889
2942
  feedbackSignals: session.sessionStory?.episodes?.flatMap((episode) => episode.feedbackSignals ?? []) ?? [],
2890
2943
  activeEventIds,
2891
- activeSourceTrace: session.sourceTrace,
2892
- activeStartRecordIndex: session.timelineScope?.segmentStartRecordIndex,
2893
- activeEndRecordIndex: session.timelineScope?.segmentEndRecordIndex,
2894
2944
  showSkillWindowMarkers: true,
2895
2945
  suppressMetricTagsOutsideSkillWindow: true,
2896
2946
  };
@@ -2927,7 +2977,14 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2927
2977
  </section>
2928
2978
  </div>`;
2929
2979
  };
2930
- const rangeText = (start, end) => start === undefined || end === undefined ? '未记录' : `#${start} - #${end}`;
2980
+ const recordRangesText = (ranges) => {
2981
+ if (!ranges || ranges.length === 0)
2982
+ return '无可定位 record';
2983
+ return ranges.map((range) => {
2984
+ const label = range.sourceTrace.split(/[\\/]/).pop() || range.traceId;
2985
+ return `${label} #${range.startRecordIndex} - #${range.endRecordIndex}(${range.eventCount} 个事件)`;
2986
+ }).join(';');
2987
+ };
2931
2988
  const renderTimelineScopeNotice = (session) => {
2932
2989
  const scope = session.timelineScope;
2933
2990
  if (!scope) {
@@ -2940,17 +2997,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2940
2997
  <button type="button" disabled title="当前 report JSON 没有 fullSessionTimeline 字段,无法在前端还原完整 session。">需要重新生成报告</button>
2941
2998
  </div>`;
2942
2999
  }
2943
- const truncatedText = scope.truncated ? '当前 skill 窗口不是完整 session 链路' : '当前展示覆盖本次 skill 窗口';
2944
- const omittedText = `前面省略 ${scope.omittedBeforeCount} 个事件,后面省略 ${scope.omittedAfterCount} 个事件`;
3000
+ const truncatedText = scope.truncated ? '当前 Skill 事件窗口不是完整 session 链路' : '当前展示覆盖本次 Skill 事件窗口';
3001
+ const omittedText = `规范时间线中,窗口前有 ${scope.omittedBeforeCount} 个事件,窗口后有 ${scope.omittedAfterCount} 个事件`;
2945
3002
  const branchCount = session.timelineTree?.branches.length ?? 0;
2946
3003
  const chainText = branchCount > 0
2947
- ? `链路结构:主线 main + ${branchCount} 条 subagent 子链路;# 编号是各自 jsonl 文件内的 message index。`
2948
- : '链路结构:单 jsonl 时间线。';
3004
+ ? `链路结构:主线 main + ${branchCount} 条 subagent 子链路;record 编号只在各自物理 trace 内有效。`
3005
+ : '链路结构:单物理 trace 时间线。';
2949
3006
  return `<div class="timeline-scope-notice" data-timeline-scope-notice>
2950
3007
  <div>
2951
3008
  <strong>${e(truncatedText)}</strong>
2952
- <span>当前 skill 窗口事件:${scope.previewEventCount} 条 / 完整 session ${scope.fullSessionEventCount} 条事件</span>
2953
- <span>record 粗范围:${e(rangeText(scope.previewStartRecordIndex, scope.previewEndRecordIndex))} · skill 窗口:${e(rangeText(scope.segmentStartRecordIndex, scope.segmentEndRecordIndex))} · 完整 session:${e(rangeText(scope.sessionStartRecordIndex, scope.sessionEndRecordIndex))} · ${e(omittedText)}</span>
3009
+ <span>当前预览:${scope.previewEventCount} 条 / Skill 事件窗口 ${scope.segmentEventCount ?? scope.previewEventCount} 条 / 完整 session ${scope.fullSessionEventCount} 条</span>
3010
+ <span>${e(omittedText)}</span>
3011
+ <span>Skill record 范围:${e(recordRangesText(scope.segmentRecordRanges))}</span>
3012
+ <span>完整 session record 范围:${e(recordRangesText(scope.sessionRecordRanges))}</span>
2954
3013
  <span>${e(chainText)}</span>
2955
3014
  </div>
2956
3015
  <button type="button" data-full-session-toggle onclick="toggleFullSessionTimeline(this)">查看完整 session 时间线</button>
@@ -3021,7 +3080,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3021
3080
  <td style="padding:8px 10px">${renderSignalLabel(item)}</td>
3022
3081
  <td class="num" style="padding:8px 10px;text-align:right">${renderOccurrences(item)}</td>
3023
3082
  <td class="num" style="padding:8px 10px;text-align:right">${item.confidence.toFixed(2)} / ${item.attributionConfidence.toFixed(2)}</td>
3024
- <td style="padding:8px 10px;color:var(--text-muted);font-size:12px">${item.lastSeen.slice(0, 19).replace('T', ' ')}</td>
3083
+ <td style="padding:8px 10px;color:var(--text-muted);font-size:12px">${e(observedItemTimestamp(item, item.lastSeen))}</td>
3025
3084
  <td style="padding:8px 10px">${renderEvidenceCell(item)}</td>
3026
3085
  <td class="num" style="padding:8px 10px;text-align:right"><button type="button" onclick="toggleObservationDetail('${detailsId}', this)" style="font-size:12px;padding:4px 8px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer">${lang === 'zh' ? '展开' : 'Details'}</button></td>
3027
3086
  </tr>
@@ -3040,8 +3099,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3040
3099
  ${renderField('signalMeaning', `原始信号=${item.signalType};OMK 判断出的失败原因=${item.signalSubtype}。例如 failed_search 表示工具搜索/读取失败,bash_probe/not_found/tool_limit 是 OMK 根据命令和输出内容进一步判断出来的原因。`)}
3041
3100
  ${renderField('occurrencesMeaning', `出现次数=${item.occurrences};按 skill/cwd/signal/subtype/query/path 聚合去重后的同类事件数量。`)}
3042
3101
  ${renderField('reviewDecision', reviewSeverityMeta(item).decision)}
3043
- ${renderField('firstSeen', item.firstSeen)}
3044
- ${renderField('lastSeen', item.lastSeen)}
3102
+ ${renderField('firstSeen', timestampedOccurrences(item) > 0 ? item.firstSeen : '未记录')}
3103
+ ${renderField('lastSeen', timestampedOccurrences(item) > 0 ? item.lastSeen : '未记录')}
3104
+ ${renderField('timestampCoverage', `${timestampedOccurrences(item)}/${item.occurrences}`)}
3045
3105
  ${renderField('recentSessionIds', item.recentSessionIds.join(', '))}
3046
3106
  <button type="button" onclick="openObservationTrace('${e(item.id)}', this)" style="margin-top:8px;font-size:12px;padding:5px 8px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer">Open in trace</button>
3047
3107
  <pre id="trace-${e(item.id)}" style="display:none;margin:8px 0 0;padding:9px;background:var(--bg-muted);border:1px solid var(--border);border-radius:6px;white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:1.45;max-height:360px;overflow:auto;text-align:left"></pre>
@@ -3092,7 +3152,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3092
3152
  low: groupItems.filter((item) => item.severity === 'low').length,
3093
3153
  noise: groupItems.filter((item) => item.severity === 'noise').length,
3094
3154
  };
3095
- const latestObservation = groupItems.reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
3155
+ const latestObservation = groupItems
3156
+ .filter((item) => timestampedOccurrences(item) > 0)
3157
+ .reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
3096
3158
  const latest = latestObservation || skillInvocationLastSeen[skillName] || '';
3097
3159
  const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
3098
3160
  const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
@@ -3136,7 +3198,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3136
3198
  <span style="font-family:ui-monospace,monospace;font-size:14px;font-weight:700">${e(skillName)}</span>
3137
3199
  <span title="这个 skill 在当前 trace 里被归因/触发的次数" style="padding:2px 7px;border-radius:999px;background:var(--info-bg);color:var(--accent);font-size:12px;font-weight:650">调用 ${invocationCount} 次</span>
3138
3200
  </div>
3139
- <div style="color:var(--text-muted);font-size:12px;margin-top:3px">${groupItems.length} 过程发现 · ${sessionCount} sessions · source ${e(sourceKinds.join(', '))} · latest ${e(latest.slice(0, 19).replace('T', ' '))}</div>
3201
+ <div style="color:var(--text-muted);font-size:12px;margin-top:3px">${groupItems.length} 过程发现 · ${sessionCount} sessions · source ${e(sourceKinds.join(', '))} · latest ${e(latest ? latest.slice(0, 19).replace('T', ' ') : '未记录')}</div>
3140
3202
  </div>
3141
3203
  <div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">
3142
3204
  <span title="高风险/需关注:优先看,可能需要补 SKILL.md 或改 skill 说明" style="padding:3px 7px;border-radius:999px;background:rgba(220,38,38,.08);color:var(--red);font-size:12px">高风险/需关注 ${counts.high}</span>
@@ -3168,7 +3230,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3168
3230
  };
3169
3231
  const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
3170
3232
  const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
3171
- const lastProblemSeen = groupItems.reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
3233
+ const lastProblemSeen = groupItems
3234
+ .filter((item) => timestampedOccurrences(item) > 0)
3235
+ .reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
3172
3236
  const lastUsed = skillInvocationLastSeen[skillName] || lastProblemSeen || '';
3173
3237
  const sources = Array.from(new Set(groupItems.map((item) => item.sourceKind))).sort();
3174
3238
  const observationCount = groupItems.length;
@@ -3497,7 +3561,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3497
3561
  <div>触发判断:${e(attributionText)}</div>
3498
3562
  ${sourceMetadataHtml}
3499
3563
  </td>
3500
- <td style="padding:9px 10px;color:var(--text-muted);font-size:12px">${e(skill.lastSeen.slice(0, 19).replace('T', ' '))}</td>
3564
+ <td style="padding:9px 10px;color:var(--text-muted);font-size:12px">${e(skillTimestampedInvocationCount(skill) > 0
3565
+ ? skill.lastSeen.slice(0, 19).replace('T', ' ')
3566
+ : '未记录')}</td>
3501
3567
  </tr>
3502
3568
  <tr id="${e(chainId)}" data-context-chain-template style="display:none">
3503
3569
  <td colspan="8">${renderSkillChainTemplate(skill.skillName)}</td>
@@ -3540,11 +3606,15 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3540
3606
  ${metric('自我纠正', indicators.selfCorrectionCount ?? 0, 'selfCorrection')} ·
3541
3607
  ${metric('重复执行', indicators.repeatedExecutionCount ?? 0, 'repeatedExecution')} ·
3542
3608
  ${metric('工具执行失败', indicators.toolFailureCount, 'toolFailure')} ·
3609
+ ${metric('工具调用取消', indicators.toolCancelledCount ?? 0, 'toolCancelled')} ·
3610
+ ${metric('工具状态未知', indicators.toolUnknownCount ?? 0, 'toolUnknown')} ·
3543
3611
  ${metric('高优先级过程发现', indicators.highObservationCount, 'highObservation')}
3544
3612
  </td>
3545
3613
  <td class="session-time-cell" style="padding:9px 10px;color:var(--text-muted);font-size:12px;white-space:normal">
3546
- <div>${e(formatTimeRange(session.sourceSessionStartTimestamp ?? session.startTimestamp, session.sourceSessionEndTimestamp ?? session.endTimestamp, session.sourceSessionDurationMs))}</div>
3547
- <div style="margin-top:3px;color:var(--text-muted);font-size:11px">${e(invocationWindowLabel)}: ${e(formatTimeRange(session.startTimestamp, session.endTimestamp))}</div>
3614
+ <div>${e(session.sourceSessionStartTimestamp || session.sourceSessionEndTimestamp
3615
+ ? formatTimeRange(session.sourceSessionStartTimestamp, session.sourceSessionEndTimestamp, session.sourceSessionDurationMs)
3616
+ : observedSessionRange(session))}</div>
3617
+ <div style="margin-top:3px;color:var(--text-muted);font-size:11px">${e(invocationWindowLabel)}: ${e(observedSessionRange(session))}</div>
3548
3618
  </td>
3549
3619
  <td class="num" style="padding:9px 10px;text-align:right"><button type="button" data-open-experience-detail onclick="event.stopPropagation(); openExperienceDetailModal('${detailId}', this, 'evidence')" style="font-size:12px;padding:5px 10px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer;white-space:nowrap">证据片段</button></td>
3550
3620
  </tr>
@@ -3571,7 +3641,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3571
3641
  <div style="font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:8px">这块只根据固定规则给“是否值得先看”的建议,例如优先复盘、抽样确认、上下文不足。它不是“符合预期/不符合预期”的最终结论。</div>
3572
3642
  ${renderAssistiveInference(shownInference)}
3573
3643
  <div style="font-size:12px;color:var(--text-secondary);line-height:1.55">
3574
- <div>调用摘要:调用段 ${session.invocationIds.length} · 工具调用 ${indicators.toolCallCount} · 工具执行失败 ${indicators.toolFailureCount} · 人工中断 ${indicators.userInterruptionCount}</div>
3644
+ <div>调用摘要:调用段 ${session.invocationIds.length} · 工具调用 ${indicators.toolCallCount} · 工具执行失败 ${indicators.toolFailureCount}${(indicators.toolCancelledCount ?? 0) > 0 ? ` · 工具调用取消 ${indicators.toolCancelledCount ?? 0}` : ''} · 人工中断 ${indicators.userInterruptionCount}</div>
3575
3645
  <div>复盘分数:${session.reviewPriorityScore}</div>
3576
3646
  <div>关联 invocation:${session.invocationIds.length}</div>
3577
3647
  <div>关联过程发现:${session.relatedObservationIds.length}</div>
@@ -3603,22 +3673,27 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3603
3673
  }, new Map()).entries())
3604
3674
  .sort((a, b) => (sessionSkillOrder.get(a[0]) ?? Number.MAX_SAFE_INTEGER) - (sessionSkillOrder.get(b[0]) ?? Number.MAX_SAFE_INTEGER));
3605
3675
  const experienceSessionGroupsHtml = experienceSessionGroups.map(([skillName, sessions], groupIndex) => {
3606
- const latest = sessions.reduce((max, session) => session.endTimestamp > max ? session.endTimestamp : max, sessions[0]?.endTimestamp ?? '');
3676
+ const timestampedSessions = sessions.filter((session) => sessionTimestampedInvocationCount(session) > 0);
3677
+ const latest = timestampedSessions.reduce((max, session) => session.endTimestamp > max ? session.endTimestamp : max, '');
3607
3678
  const earliestSessionStart = sessions.reduce((min, session) => {
3608
- const value = session.sourceSessionStartTimestamp ?? session.startTimestamp;
3679
+ const value = session.sourceSessionStartTimestamp
3680
+ ?? (sessionTimestampedInvocationCount(session) > 0 ? session.startTimestamp : '');
3609
3681
  return value && value < min ? value : min;
3610
- }, sessions[0]?.sourceSessionStartTimestamp ?? sessions[0]?.startTimestamp ?? '');
3682
+ }, '');
3611
3683
  const latestSessionEnd = sessions.reduce((max, session) => {
3612
- const value = session.sourceSessionEndTimestamp ?? session.endTimestamp;
3684
+ const value = session.sourceSessionEndTimestamp
3685
+ ?? (sessionTimestampedInvocationCount(session) > 0 ? session.endTimestamp : '');
3613
3686
  return value && value > max ? value : max;
3614
- }, sessions[0]?.sourceSessionEndTimestamp ?? sessions[0]?.endTimestamp ?? '');
3687
+ }, '');
3615
3688
  const reviewFirst = sessions.filter((session) => inboxResolvedPriority(session) === 'review_first').length;
3616
3689
  const sampleReview = sessions.filter((session) => inboxResolvedPriority(session) === 'sample_review').length;
3617
3690
  return `<details id="${e(experienceSkillAnchor(skillName))}" class="experience-session-group" open style="scroll-margin-top:16px">
3618
3691
  <summary>
3619
3692
  <div>
3620
3693
  <span class="experience-session-skill">${e(skillName)}</span>
3621
- <span class="experience-session-meta">${sessions.length} sessions · ${e(sessionTimeLabel)} ${e(formatTimeRange(earliestSessionStart, latestSessionEnd))} · ${e(latestInvocationLabel)} ${e(formatTimestamp(latest))}</span>
3694
+ <span class="experience-session-meta">${sessions.length} sessions · ${e(sessionTimeLabel)} ${e(earliestSessionStart || latestSessionEnd
3695
+ ? formatTimeRange(earliestSessionStart, latestSessionEnd)
3696
+ : (lang === 'zh' ? '未记录' : 'Not recorded'))} · ${e(latestInvocationLabel)} ${e(latest ? formatTimestamp(latest) : (lang === 'zh' ? '未记录' : 'Not recorded'))}</span>
3622
3697
  </div>
3623
3698
  <div class="experience-session-tags">
3624
3699
  <span style="color:var(--red)">优先复盘 ${reviewFirst}</span>
@@ -3647,16 +3722,6 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3647
3722
  </details>`;
3648
3723
  }).join('');
3649
3724
  void experienceSessionGroupsHtml;
3650
- const experienceIndicators = experience?.sessions.reduce((acc, session) => {
3651
- const indicators = displayIndicatorsForSession(session);
3652
- return {
3653
- toolCallCount: acc.toolCallCount + indicators.toolCallCount,
3654
- toolFailureCount: acc.toolFailureCount + indicators.toolFailureCount,
3655
- userInterruptionCount: acc.userInterruptionCount + indicators.userInterruptionCount,
3656
- };
3657
- }, { toolCallCount: 0, toolFailureCount: 0, userInterruptionCount: 0 }) ?? { toolCallCount: 0, toolFailureCount: 0, userInterruptionCount: 0 };
3658
- const experienceToolSuccessCount = Math.max(0, experienceIndicators.toolCallCount - experienceIndicators.toolFailureCount);
3659
- void experienceToolSuccessCount;
3660
3725
  const experienceReviewSkillCount = experience?.skills.filter((skill) => skill.reviewFirstSessionCount + skill.sampleReviewSessionCount > 0).length ?? 0;
3661
3726
  void experienceReviewSkillCount;
3662
3727
  const experienceReviewSessionCount = experience?.sessions.filter((session) => inboxResolvedPriority(session) !== 'routine_sample').length ?? 0;
@@ -3707,7 +3772,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3707
3772
  inboxSiblingsMap.set(skill.sessionId, arr);
3708
3773
  }
3709
3774
  for (const arr of inboxSiblingsMap.values()) {
3710
- arr.sort((a, b) => a.startTimestamp.localeCompare(b.startTimestamp));
3775
+ arr.sort((a, b) => Number(sessionTimestampedInvocationCount(b) > 0) - Number(sessionTimestampedInvocationCount(a) > 0)
3776
+ || a.startTimestamp.localeCompare(b.startTimestamp));
3711
3777
  }
3712
3778
  const inboxSkillsMap = new Map();
3713
3779
  for (const s of inboxSessions) {
@@ -3717,13 +3783,16 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3717
3783
  }
3718
3784
  const inboxPriorityRank = (priority) => priority === 'review_first' ? 0 : priority === 'sample_review' ? 1 : 2;
3719
3785
  const inboxSkillCards = Array.from(inboxSkillsMap.entries()).map(([skillName, sessions]) => {
3720
- sessions.sort((a, b) => b.endTimestamp.localeCompare(a.endTimestamp));
3786
+ sessions.sort((a, b) => Number(sessionTimestampedInvocationCount(b) > 0) - Number(sessionTimestampedInvocationCount(a) > 0)
3787
+ || b.endTimestamp.localeCompare(a.endTimestamp));
3721
3788
  const priority = sessions.find((s) => inboxResolvedPriority(s) === 'review_first')
3722
3789
  ? 'review_first'
3723
3790
  : sessions.find((s) => inboxResolvedPriority(s) === 'sample_review')
3724
3791
  ? 'sample_review'
3725
3792
  : 'routine_sample';
3726
- const latestEnd = sessions.reduce((latest, s) => s.endTimestamp > latest ? s.endTimestamp : latest, '');
3793
+ const latestEnd = sessions
3794
+ .filter((session) => sessionTimestampedInvocationCount(session) > 0)
3795
+ .reduce((latest, s) => s.endTimestamp > latest ? s.endTimestamp : latest, '');
3727
3796
  return { skillName, sessions, priority, latestEnd };
3728
3797
  });
3729
3798
  inboxSkillCards.sort((a, b) => inboxPriorityRank(a.priority) - inboxPriorityRank(b.priority) || b.latestEnd.localeCompare(a.latestEnd) || a.skillName.localeCompare(b.skillName));
@@ -3883,7 +3952,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3883
3952
  };
3884
3953
  const inboxEntrypointShort = (session) => formatEntrypoint(session.entrypoint) || '未记录';
3885
3954
  const inboxFormatSessionLabel = (session) => {
3886
- const time = session.endTimestamp ? session.endTimestamp.slice(0, 16).replace('T', ' ') : '';
3955
+ const time = sessionTimestampedInvocationCount(session) > 0
3956
+ ? observedSessionTimestamp(session, session.endTimestamp).slice(0, 16)
3957
+ : '';
3887
3958
  const entrypoint = inboxEntrypointShort(session);
3888
3959
  const sessionShort = session.sessionId.length > 10 ? `${session.sessionId.slice(0, 8)}…` : session.sessionId;
3889
3960
  return [`Session ${sessionShort}`, entrypoint !== '未记录' ? entrypoint : '', time].filter(Boolean).join(' · ') || 'Session 调用记录';
@@ -4003,8 +4074,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4003
4074
  const roleLabel = link ? inboxSkillRoleLabel(link.role) : '执行';
4004
4075
  const roleHelpKey = inboxSkillRoleHelpKey(link?.role ?? 'executor');
4005
4076
  const indicators = displayIndicatorsForSession(skill);
4006
- const dur = formatTimeRange(skill.startTimestamp, skill.endTimestamp);
4007
- const startTime = skill.startTimestamp ? skill.startTimestamp.slice(5, 16).replace('T', ' ') : '未记录';
4077
+ const dur = observedSessionRange(skill);
4078
+ const startTime = sessionTimestampedInvocationCount(skill) > 0
4079
+ ? skill.startTimestamp.slice(5, 16).replace('T', ' ')
4080
+ : '未记录';
4008
4081
  const resolvedPriority = inboxResolvedPriority(skill);
4009
4082
  const priorityLabel = resolvedPriority === 'review_first' ? '要看一眼' : resolvedPriority === 'sample_review' ? '抽样' : '常规';
4010
4083
  const priorityCls = resolvedPriority === 'review_first' ? 'is-priority-high' : resolvedPriority === 'sample_review' ? 'is-priority-medium' : 'is-priority-low';
@@ -4025,6 +4098,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4025
4098
  <span>调用段 ${skill.invocationIds.length}</span>
4026
4099
  <span>工具 ${indicators.toolCallCount}</span>
4027
4100
  <span>失败 ${indicators.toolFailureCount}</span>
4101
+ ${(indicators.toolCancelledCount ?? 0) > 0 ? `<span>取消 ${indicators.toolCancelledCount ?? 0}</span>` : ''}
4102
+ ${(indicators.toolUnknownCount ?? 0) > 0 ? `<span>状态未知 ${indicators.toolUnknownCount ?? 0}</span>` : ''}
4028
4103
  </div>
4029
4104
  <div class="inbox-flow-range">${e(dur)}</div>
4030
4105
  </div>
@@ -4777,7 +4852,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4777
4852
  const out = {};
4778
4853
  for (const inv of inboxGetSessionInvocations(session)) {
4779
4854
  for (const [k, v] of Object.entries(inv.toolCounts ?? {}))
4780
- out[k] = (out[k] ?? 0) + v;
4855
+ incrementRecordCount(out, k, v);
4781
4856
  }
4782
4857
  return out;
4783
4858
  };
@@ -4793,7 +4868,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4793
4868
  for (const inv of invocations) {
4794
4869
  for (const event of inv.timeline) {
4795
4870
  if (event.kind === 'tool_result' && event.isError && event.toolName) {
4796
- failures[event.toolName] = (failures[event.toolName] ?? 0) + 1;
4871
+ incrementRecordCount(failures, event.toolName);
4797
4872
  }
4798
4873
  }
4799
4874
  }
@@ -4959,15 +5034,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4959
5034
  skill.sourceMetadata?.model,
4960
5035
  skill.sourceMetadata?.modelApi,
4961
5036
  ].filter(Boolean).join(' / ') || '未记录模型';
4962
- const tokenDetail = metrics?.tokenUsage ? [
4963
- { name: '输入 token', count: metrics.tokenUsage.inputTokens },
4964
- { name: '输出 token', count: metrics.tokenUsage.outputTokens },
4965
- { name: 'cache 读取', count: metrics.tokenUsage.cacheReadTokens },
4966
- { name: 'cache 写入', count: metrics.tokenUsage.cacheCreationTokens },
5037
+ const tokenUsage = metrics?.tokenUsage;
5038
+ const tokenCoverage = tokenUsage?.coverage ?? 0;
5039
+ const tokenDetail = tokenCoverage > 0 ? [
5040
+ { name: '输入 token', count: tokenUsage.inputTokens },
5041
+ { name: '输出 token', count: tokenUsage.outputTokens },
5042
+ { name: 'cache 读取', count: tokenUsage.cacheReadTokens },
5043
+ { name: 'cache 写入', count: tokenUsage.cacheCreationTokens },
4967
5044
  ] : [];
4968
5045
  const cards = !metrics ? [] : [
4969
5046
  { key: 'toolCall', label: '工具调用', value: indicators.toolCallCount, detail: toolDetail, note: '本次能力调用段内触发的工具调用总数及各类工具的命中分布。', anomaly: false },
4970
5047
  { key: 'toolFailure', label: '工具失败', value: indicators.toolFailureCount, detail: toolFailureDetail, note: '工具执行返回失败的次数。命中失败可在版块 ④ 看具体上下文。', anomaly: indicators.toolFailureCount > 0 },
5048
+ { key: 'toolCancelled', label: '工具取消', value: indicators.toolCancelledCount ?? 0, detail: [], note: 'runtime 明确取消的工具调用次数。取消与执行失败分开统计。', anomaly: false },
5049
+ { key: 'toolUnknown', label: '状态未知', value: indicators.toolUnknownCount ?? 0, detail: [], note: 'runtime 没有提供可信终态的工具调用次数。这些调用不计入工具成功率或失败率分母。', anomaly: false },
4971
5050
  { key: 'userMessage', label: '用户消息', value: indicators.userMessageCount, detail: [], note: '本次能力调用段内的真实人工用户消息条数(已剔除 Skill 文档注入和运行时注入)。', anomaly: false },
4972
5051
  { key: 'userFollowUp', label: '追问', value: indicators.userFollowUpCount, detail: [], note: '按当前 skill 的归因结果统计用户追问 / 补充;点击版块 ④ 可用同名标签定位原文。', anomaly: indicators.userFollowUpCount > 0 },
4973
5052
  { key: 'userCorrection', label: '纠正', value: indicators.userCorrectionCount, detail: [], note: '用户明确纠正、否决前一轮交付的次数。出现即建议进版块 ④ 看上下文。', anomaly: indicators.userCorrectionCount > 0 },
@@ -4978,8 +5057,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4978
5057
  { key: 'progress', label: '过程进展', value: metrics.assistantProgressUpdateCount ?? 0, detail: [], note: '回答中出现"正在 / 仍在 / 进度更新"等过程进展信号的次数。这类不算最终交付。', anomaly: false },
4979
5058
  { key: 'selfCorrection', label: '自我纠正', value: metrics.selfCorrectionCount ?? indicators.selfCorrectionCount ?? 0, detail: [], note: 'agent 在没有用户介入的情况下发现问题并主动修正执行策略。少量说明有恢复能力,高频说明流程不稳。', anomaly: (metrics.selfCorrectionCount ?? indicators.selfCorrectionCount ?? 0) > 0 },
4980
5059
  { key: 'repeatedExecution', label: '重复执行', value: metrics.repeatedExecutionCount ?? indicators.repeatedExecutionCount ?? 0, detail: [], note: '同类步骤、工具或流程被重复执行。高频出现时通常对应绕路或 workflow 不清晰。', anomaly: (metrics.repeatedExecutionCount ?? indicators.repeatedExecutionCount ?? 0) > 0 },
4981
- { key: 'tokenInput', label: '输入 token', value: metrics.tokenUsage?.inputTokens ?? 0, detail: tokenDetail, note: '按本次能力调用段累计的 token 用量。', anomaly: false },
4982
- { key: 'tokenOutput', label: '输出 token', value: metrics.tokenUsage?.outputTokens ?? 0, detail: tokenDetail, note: '按本次能力调用段累计的 token 用量。', anomaly: false },
5060
+ ...(tokenCoverage > 0 ? [
5061
+ { key: 'tokenInput', label: '输入 token', value: tokenUsage.inputTokens, detail: tokenDetail, note: `仅累计 trace 已上报 token 的能力调用,覆盖率 ${Math.round(tokenCoverage * 100)}%。`, anomaly: false },
5062
+ { key: 'tokenOutput', label: '输出 token', value: tokenUsage.outputTokens, detail: tokenDetail, note: `仅累计 trace 已上报 token 的能力调用,覆盖率 ${Math.round(tokenCoverage * 100)}%。`, anomaly: false },
5063
+ ] : []),
4983
5064
  ];
4984
5065
  const metricRow = cards.length === 0
4985
5066
  ? `<div id="inbox-sec-runtime-metrics-${e(skill.id)}"><p class="inbox-skill-empty">这条能力调用没有运行指标。</p></div>`
@@ -5044,11 +5125,22 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5044
5125
  }
5045
5126
  if (indicators.toolFailureCount > 0)
5046
5127
  runtimeIssues.push(`工具失败 ${indicators.toolFailureCount} 次`);
5047
- const runtimeSummaryText = runtimeIssues.length > 0 ? runtimeIssues.join(' · ') : '运行指标无异常';
5048
- const runtimeSummaryClass = runtimeIssues.length > 0 ? 'is-attention' : 'is-ok';
5128
+ const cancelledToolOutcomes = indicators.toolCancelledCount ?? 0;
5129
+ const unknownToolOutcomes = indicators.toolUnknownCount ?? 0;
5130
+ const runtimeSummaryText = runtimeIssues.length > 0
5131
+ ? runtimeIssues.join(' · ')
5132
+ : cancelledToolOutcomes > 0 || unknownToolOutcomes > 0
5133
+ ? [
5134
+ cancelledToolOutcomes > 0 ? `${cancelledToolOutcomes} 次工具调用取消` : '',
5135
+ unknownToolOutcomes > 0 ? `${unknownToolOutcomes} 次工具结果状态未知` : '',
5136
+ ].filter(Boolean).join(' · ')
5137
+ : '运行指标无异常';
5138
+ const runtimeSummaryClass = runtimeIssues.length > 0
5139
+ ? 'is-attention'
5140
+ : unknownToolOutcomes > 0 ? 'is-neutral' : 'is-ok';
5049
5141
  const flowSummaryText = siblings.length > 1 ? `${siblings.length} 个能力调用段` : '单一能力调用段';
5050
5142
  const flowTemplateId = `inbox-flow-template-${safeId}`;
5051
- const evidenceSummaryText = `工具 ${indicators.toolCallCount} · 失败 ${indicators.toolFailureCount} · 用户消息 ${indicators.userMessageCount}`;
5143
+ const evidenceSummaryText = `工具 ${indicators.toolCallCount} · 失败 ${indicators.toolFailureCount}${cancelledToolOutcomes > 0 ? ` · 取消 ${cancelledToolOutcomes}` : ''}${unknownToolOutcomes > 0 ? ` · 状态未知 ${unknownToolOutcomes}` : ''} · 用户消息 ${indicators.userMessageCount}`;
5052
5144
  const navItems = [
5053
5145
  { id: `inbox-sec-completion-${safeId}`, label: '① 这次跑得怎么样' },
5054
5146
  { id: `inbox-sec-log-chain-${safeId}`, label: '② 日志上下游链路' },
@@ -5210,6 +5302,25 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5210
5302
  <div style="color:var(--text-muted);font-size:12px">当前只展示最新一次 ingest 的结果</div>
5211
5303
  </div>
5212
5304
  </section>`;
5305
+ const ingestionIssues = reports.reduce((totals, report) => {
5306
+ const ingestion = report.meta.ingestion;
5307
+ if (!ingestion)
5308
+ return totals;
5309
+ totals.malformed += ingestion.malformedRecordCount;
5310
+ totals.ignored += ingestion.ignoredValueCount;
5311
+ totals.unknown += ingestion.unknownEventCount;
5312
+ return totals;
5313
+ }, { malformed: 0, ignored: 0, unknown: 0 });
5314
+ const ingestionNotice = ingestionIssues.malformed > 0
5315
+ || ingestionIssues.ignored > 0
5316
+ || ingestionIssues.unknown > 0
5317
+ ? `<div style="margin:12px 0;padding:10px 12px;border:1px solid var(--yellow);border-radius:8px;background:var(--yellow-bg);color:var(--text-secondary);font-size:13px">
5318
+ <strong style="color:var(--yellow)">${lang === 'zh' ? '观测输入需要复核' : 'Observation input needs review'}</strong>
5319
+ <span style="margin-left:8px">${lang === 'zh'
5320
+ ? `${ingestionIssues.malformed} 条格式损坏记录,${ingestionIssues.ignored} 个非对象值,${ingestionIssues.unknown} 个未识别事件。`
5321
+ : `${ingestionIssues.malformed} malformed records, ${ingestionIssues.ignored} non-object values, ${ingestionIssues.unknown} unrecognized events.`}</span>
5322
+ </div>`
5323
+ : '';
5213
5324
  return layout(pageTitle, `
5214
5325
  <main class="observe-report-root">
5215
5326
  <nav style="margin-bottom:12px"><a href="/observe-health" style="color:var(--accent);text-decoration:none">${lang === 'zh' ? '能力健康度日报' : 'Skill health reports'}</a></nav>
@@ -5220,6 +5331,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5220
5331
  </div>
5221
5332
  ${activeSkill ? `<a href="/observe-inbox" style="color:var(--accent);text-decoration:none;font-size:13px">查看全量</a>` : ''}
5222
5333
  </div>
5334
+ ${ingestionNotice}
5223
5335
  <style>${OBSERVATION_INBOX_STYLES}</style>
5224
5336
  <div id="signal-global-tooltip" role="tooltip"></div>
5225
5337
  <div id="timeline-fulltext-tooltip" role="dialog" aria-modal="true" aria-hidden="true" aria-label="时间线消息详情"></div>
@@ -6585,10 +6697,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6585
6697
  targetId: targetId,
6586
6698
  verdict: 'reviewed',
6587
6699
  note: action,
6700
+ traceId: btn ? btn.getAttribute('data-trace-id') || undefined : undefined,
6588
6701
  sourceTrace: btn ? btn.getAttribute('data-source-trace') || undefined : undefined,
6589
6702
  sessionId: btn ? btn.getAttribute('data-session-id') || undefined : undefined,
6590
6703
  messageIndex: btn && btn.getAttribute('data-message-index') ? Number(btn.getAttribute('data-message-index')) : undefined,
6591
6704
  messageUuid: btn ? btn.getAttribute('data-message-uuid') || undefined : undefined,
6705
+ callInstanceId: btn ? btn.getAttribute('data-call-instance-id') || undefined : undefined,
6592
6706
  toolUseId: btn ? btn.getAttribute('data-tool-use-id') || undefined : undefined,
6593
6707
  snippet: btn ? btn.getAttribute('data-snippet') || undefined : undefined
6594
6708
  })
@@ -6721,10 +6835,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6721
6835
  verdict: next,
6722
6836
  metricKey: metric.metricKey,
6723
6837
  metricScopeId: metric.metricScopeId || undefined,
6838
+ traceId: source.traceId || undefined,
6724
6839
  sourceTrace: source.sourceTrace || undefined,
6725
6840
  sessionId: source.sessionId || undefined,
6726
6841
  messageIndex: source.messageIndex === undefined ? undefined : Number(source.messageIndex),
6727
6842
  messageUuid: source.messageUuid || undefined,
6843
+ callInstanceId: source.callInstanceId || undefined,
6728
6844
  toolUseId: source.toolUseId || undefined,
6729
6845
  snippet: source.snippet || undefined
6730
6846
  })
@@ -6851,10 +6967,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6851
6967
  targetId: targetId,
6852
6968
  verdict: next,
6853
6969
  metricKey: metricKey,
6970
+ traceId: btn.getAttribute('data-trace-id') || undefined,
6854
6971
  sourceTrace: btn.getAttribute('data-source-trace') || undefined,
6855
6972
  sessionId: btn.getAttribute('data-session-id') || undefined,
6856
6973
  messageIndex: btn.getAttribute('data-message-index') ? Number(btn.getAttribute('data-message-index')) : undefined,
6857
6974
  messageUuid: btn.getAttribute('data-message-uuid') || undefined,
6975
+ callInstanceId: btn.getAttribute('data-call-instance-id') || undefined,
6858
6976
  toolUseId: btn.getAttribute('data-tool-use-id') || undefined,
6859
6977
  snippet: btn.getAttribute('data-snippet') || undefined,
6860
6978
  reason: reason || undefined