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,12 +1,14 @@
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';
8
+ import { shellQuoteArg } from '../shared/shell-quote.js';
7
9
  export { renderFeedbackAttributionLabel } from './observation-inbox/helpers.js';
8
10
  export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
9
- const { activeSkill, allItems, items, reports, experienceReports, skillInvocationCounts, skillSessionCounts, skillInvocationLastSeen, skillToolCallCounts, skillChains, skillDerivedStandards, totalSkillInvocations, severitySkillCounts, skillCount, reportCount, latestSeenLabel, reviewState, resolvedReviewSessions, } = model;
11
+ const { observationsDir, activeSkill, allItems, items, reports, experienceReports, skillInvocationCounts, skillSessionCounts, skillInvocationLastSeen, skillToolCallCounts, skillChains, skillDerivedStandards, totalSkillInvocations, severitySkillCounts, skillCount, reportCount, latestSeenLabel, reviewState, resolvedReviewSessions, } = model;
10
12
  const experience = experienceReports.find((report) => report.skills.length > 0 || report.sessions.length > 0 || report.invocations.length > 0);
11
13
  const pageTitle = activeSkill ? `Observe Inbox · ${activeSkill}` : 'Observe Inbox';
12
14
  const experienceToolCountsBySkill = new Map();
@@ -16,18 +18,18 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
16
18
  for (const invocation of experience?.invocations ?? []) {
17
19
  const toolCounts = experienceToolCountsBySkill.get(invocation.skillName) ?? {};
18
20
  for (const [tool, count] of Object.entries(invocation.toolCounts ?? {})) {
19
- toolCounts[tool] = (toolCounts[tool] ?? 0) + count;
21
+ incrementRecordCount(toolCounts, tool, count);
20
22
  }
21
23
  experienceToolCountsBySkill.set(invocation.skillName, toolCounts);
22
24
  const entrypoint = invocation.entrypoint ?? invocation.sourceKind ?? 'unknown';
23
25
  const entrypointCounts = experienceEntrypointCountsBySkill.get(invocation.skillName) ?? {};
24
- entrypointCounts[entrypoint] = (entrypointCounts[entrypoint] ?? 0) + 1;
26
+ incrementRecordCount(entrypointCounts, entrypoint);
25
27
  experienceEntrypointCountsBySkill.set(invocation.skillName, entrypointCounts);
26
28
  const originLabel = invocation.attribution.pluginName
27
29
  ? `Skill 来源:插件 ${invocation.attribution.pluginName}`
28
30
  : 'Skill 来源:本地 skill';
29
31
  const originCounts = experienceSkillOriginCountsBySkill.get(invocation.skillName) ?? {};
30
- originCounts[originLabel] = (originCounts[originLabel] ?? 0) + 1;
32
+ incrementRecordCount(originCounts, originLabel);
31
33
  experienceSkillOriginCountsBySkill.set(invocation.skillName, originCounts);
32
34
  const attributionMethod = invocation.attribution.source === 'command-name'
33
35
  ? '通过斜杠命令触发'
@@ -43,7 +45,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
43
45
  const attributionTarget = invocation.attribution.commandName ?? invocation.attribution.rawSkillRef ?? invocation.skillName;
44
46
  const attributionLabel = attributionTarget ? `${attributionMethod}:${attributionTarget}` : attributionMethod;
45
47
  const attributionCounts = experienceAttributionCountsBySkill.get(invocation.skillName) ?? {};
46
- attributionCounts[attributionLabel] = (attributionCounts[attributionLabel] ?? 0) + 1;
48
+ incrementRecordCount(attributionCounts, attributionLabel);
47
49
  experienceAttributionCountsBySkill.set(invocation.skillName, attributionCounts);
48
50
  }
49
51
  const countSkillsBySeverity = (...severities) => new Set(allItems.filter((item) => severities.includes(item.severity)).map((item) => item.skillName)).size;
@@ -52,6 +54,27 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
52
54
  const latestInvocationLabel = lang === 'zh' ? '最近调用' : 'Latest invocation';
53
55
  const invocationWindowLabel = lang === 'zh' ? '调用窗口' : 'Invocation window';
54
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);
55
78
  const reviewSeverityMeta = (item) => {
56
79
  if (item.severity === 'high') {
57
80
  return { label: '高风险/需关注', decision: '优先看,可能要补 SKILL.md 或改 skill 说明', color: 'var(--red)', bg: 'rgba(220,38,38,.08)' };
@@ -165,8 +188,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
165
188
  </div>`;
166
189
  };
167
190
  const renderSourceBadge = (item) => {
168
- const label = item.sourceKind === 'openclaw' ? 'OpenClaw' : item.sourceKind === 'markdown_log' ? 'Markdown log' : item.sourceKind === 'claude' ? 'Claude' : 'Unknown';
169
- 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)';
170
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>`;
171
194
  };
172
195
  const confidenceHeaderHelp = '判断把握:OMK 对“这条 过程发现 是否需要处理/是否高风险/需关注”的规则判断有多确定。归属把握:OMK 把这条 过程发现 归到当前 skill 名下有多确定,例如明确调用 skill 通常更高。';
@@ -182,6 +205,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
182
205
  repeatedExecution: '重复执行',
183
206
  toolCall: '工具调用',
184
207
  toolFailure: '工具执行失败',
208
+ toolCancelled: '工具调用取消',
209
+ toolUnknown: '工具状态未知',
185
210
  highObservation: '高优先级过程发现',
186
211
  mediumObservation: '低风险/抽样过程发现',
187
212
  hedging: '不确定表达',
@@ -215,6 +240,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
215
240
  repeatedExecution: '统计同类步骤、工具或流程被重复执行的信号。高频出现通常对应绕路、工具策略不清晰或 workflow 缺少明确顺序。',
216
241
  toolCall: '统计该 skill 运行片段里的 tool_use 调用总数,包括 Bash、Read、Grep 等工具。',
217
242
  toolFailure: '统计该 skill 运行片段里失败的工具执行结果,例如 tool_result 标记 is_error=true。注意:工具执行失败不等于整个 skill 调用失败。',
243
+ toolCancelled: '统计 runtime 明确标记为 cancelled 的工具调用。取消与工具执行失败分开统计,也不作为知识缺口证据。',
244
+ toolUnknown: '统计 runtime 未提供可信终态的工具调用。状态未知不计入工具成功率或失败率分母。',
218
245
  highObservation: '统计 severity=high 的过程发现,通常表示可能需要优先复盘的执行问题。',
219
246
  mediumObservation: '统计 severity=medium/low 的过程发现,通常进入抽样复盘,不直接等同于必须改 skill。',
220
247
  hedging: '统计回答或过程发现里的“不确定 / 可能 / 需要确认”等低置信文本信号。',
@@ -242,6 +269,11 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
242
269
  'claude-desktop': 'Claude Code App',
243
270
  cli: 'Claude CLI',
244
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',
245
277
  openclaw: 'OpenClaw',
246
278
  markdown_log: 'Markdown 日志',
247
279
  };
@@ -265,15 +297,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
265
297
  const renderInvocationSummary = (indicators, invocationCount) => {
266
298
  const total = indicators.toolCallCount;
267
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);
268
302
  const interrupted = Math.max(0, indicators.userInterruptionCount);
269
- const success = Math.max(0, total - failed);
303
+ const success = Math.max(0, total - failed - cancelled - unknown);
270
304
  const pct = (value) => total > 0 ? `${Math.round(value / total * 100)}%` : '—';
271
305
  return `<div class="invocation-summary" title="这是 trace 中可观测到的调用过程汇总。工具执行失败/人工中断是过程信号,不直接等同于整个 skill 失败或用户目标失败。">
272
306
  <div class="invocation-total">工具调用总次数 <strong>${total}</strong></div>
273
307
  <div class="invocation-breakdown">
274
308
  <span>工具执行成功 ${success} / ${pct(success)}</span>
275
309
  <span>工具执行失败 ${failed} / ${pct(failed)}</span>
276
- <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>
277
313
  </div>
278
314
  <div class="invocation-footnote">Skill 调用段 ${invocationCount}</div>
279
315
  </div>`;
@@ -342,7 +378,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
342
378
  const next = {};
343
379
  for (const [key, count] of Object.entries(counts ?? {})) {
344
380
  const display = label(key);
345
- next[display] = (next[display] ?? 0) + count;
381
+ incrementRecordCount(next, display, count);
346
382
  }
347
383
  return next;
348
384
  };
@@ -410,7 +446,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
410
446
  if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
411
447
  for (const [key, value] of Object.entries(legacy)) {
412
448
  if (typeof value === 'number')
413
- merged[key] = (merged[key] ?? 0) + value;
449
+ incrementRecordCount(merged, key, value);
414
450
  }
415
451
  }
416
452
  return merged;
@@ -504,6 +540,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
504
540
  repeatedExecutionCount: 0,
505
541
  toolCallCount: 0,
506
542
  toolFailureCount: 0,
543
+ toolCancelledCount: 0,
544
+ toolUnknownCount: 0,
507
545
  highObservationCount: 0,
508
546
  mediumObservationCount: 0,
509
547
  hedgingCount: 0,
@@ -515,8 +553,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
515
553
  return pattern.test(value);
516
554
  };
517
555
  const displayMetricVerdict = (event, metricKey, metricScopeId) => {
518
- const targetId = observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
519
- const entry = reviewState.entries[`evidence_metric:${targetId}`];
556
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
520
557
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
521
558
  };
522
559
  const displayMetricIsActive = (event, metricKey, ruleDetected, metricScopeId) => {
@@ -719,6 +756,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
719
756
  repeatedExecutionCount: current.repeatedExecutionCount + (next.repeatedExecutionCount ?? 0),
720
757
  toolCallCount: current.toolCallCount + next.toolCallCount,
721
758
  toolFailureCount: current.toolFailureCount + next.toolFailureCount,
759
+ toolCancelledCount: (current.toolCancelledCount ?? 0) + (next.toolCancelledCount ?? 0),
760
+ toolUnknownCount: (current.toolUnknownCount ?? 0) + (next.toolUnknownCount ?? 0),
722
761
  highObservationCount: current.highObservationCount + next.highObservationCount,
723
762
  mediumObservationCount: current.mediumObservationCount + next.mediumObservationCount,
724
763
  hedgingCount: current.hedgingCount + next.hedgingCount,
@@ -869,8 +908,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
869
908
  const displayToolCounts = mergeCountsByLabel(toolCounts, userFacingToolLabel);
870
909
  const total = indicators.toolCallCount;
871
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);
872
913
  const interrupted = Math.max(0, indicators.userInterruptionCount);
873
- const success = Math.max(0, total - failed);
914
+ const success = Math.max(0, total - failed - cancelled - unknown);
874
915
  const processMetrics = [
875
916
  renderDecisionMetricIfPositive('高优先级', indicators.highObservationCount, 'priority'),
876
917
  renderDecisionMetricIfPositive('抽样过程发现', indicators.mediumObservationCount, 'sample'),
@@ -881,9 +922,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
881
922
  renderDecisionMetricIfPositive('显式缺口', indicators.explicitMarkerCount, 'sample'),
882
923
  ].filter(Boolean).join('');
883
924
  return `<div class="skill-evidence-summary">
884
- <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>
885
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>
886
- <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>
887
928
  <div class="summary-row"><span class="summary-title">【过程发现】</span>${processMetrics || '<span class="summary-muted">未发现需要展示的过程信号</span>'}</div>
888
929
  ${renderSkillProblemPatterns(skill.skillName, skill.problemPatterns)}
889
930
  </div>`;
@@ -1855,6 +1896,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
1855
1896
  };
1856
1897
  const metrics = report.oneLookMetrics;
1857
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 用量)';
1858
1903
  const traceLinks = report.traceLinks.slice(0, 8);
1859
1904
  const story = report.sessionStory ?? fallbackSessionStory(report);
1860
1905
  const storyEvidenceButtons = (refs) => refs.length > 0
@@ -2148,6 +2193,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2148
2193
  ${[
2149
2194
  ['工具调用', metrics.toolCallCount],
2150
2195
  ['工具失败', metrics.toolFailureCount],
2196
+ ['工具取消', metrics.toolCancelledCount ?? 0],
2197
+ ['状态未知', metrics.toolUnknownCount ?? 0],
2151
2198
  ['用户消息', metrics.userMessageCount],
2152
2199
  ['追问/补充', metrics.userFollowUpCount],
2153
2200
  ['有结果', metrics.assistantDeliverySignalCount],
@@ -2158,7 +2205,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2158
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('')}
2159
2206
  </div>
2160
2207
  <div style="font-size:11px;color:var(--text-muted);line-height:1.5;margin-bottom:8px">
2161
- Token 使用量(按本次能力片段归因):输入 ${tokenUsage.inputTokens} / 输出 ${tokenUsage.outputTokens} / 缓存读取 ${tokenUsage.cacheReadTokens} / 缓存写入 ${tokenUsage.cacheCreationTokens}
2208
+ Token 使用量(按本次能力片段归因):${tokenUsageText}
2162
2209
  </div>
2163
2210
  <div style="font-size:12px;font-weight:700;color:var(--text-primary);margin-bottom:6px">证据定位</div>
2164
2211
  <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
@@ -2334,8 +2381,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2334
2381
  if (event.kind === 'tool_use')
2335
2382
  return { icon: 'TU', label: '工具调用', tone: 'tool-use' };
2336
2383
  if (event.kind === 'tool_result') {
2337
- if (event.isError)
2384
+ if (event.toolStatus === 'failure' || (event.toolStatus === undefined && event.isError))
2338
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' };
2339
2390
  return { icon: 'TR', label: isSkillLaunchResult(event) ? '工具调用成功' : '工具执行成功', tone: 'tool-result' };
2340
2391
  }
2341
2392
  if (event.kind === 'skill_context')
@@ -2377,8 +2428,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2377
2428
  return /重复(?:执行|尝试|读取|搜索|调用)|再次(?:执行|读取|搜索|调用)|重新(?:执行|读取|搜索|调用|跑|运行)|再(?:执行|读取|搜索|调用|跑)一遍|重试|\b(?:retry|rerun)\b/i.test(text);
2378
2429
  }
2379
2430
  const evidenceMetricVerdictFor = (event, metricKey, metricScopeId) => {
2380
- const targetId = observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
2381
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2431
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2382
2432
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
2383
2433
  };
2384
2434
  const evidenceMetricIsActive = (event, metricKey, ruleDetected, metricScopeId) => {
@@ -2557,9 +2607,15 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2557
2607
  if (event.kind === 'tool_result') {
2558
2608
  if (evidenceMetricIsActive(event, 'repeated_execution', hasRepeatedExecutionSignal(event)))
2559
2609
  badges.push({ label: evidenceMetricBadgeLabel('重复执行', event, 'repeated_execution', hasRepeatedExecutionSignal(event)), className: 'metric-repeated-execution', title: '同类工具结果或失败恢复路径被重复出现。高频出现时通常对应绕路或 workflow 不清晰。' });
2560
- if (event.isError) {
2610
+ if (event.toolStatus === 'failure' || (event.toolStatus === undefined && event.isError)) {
2561
2611
  badges.push({ label: '工具执行失败', className: 'metric-tool-failure', title: 'tool_result 标记 is_error=true,表示这次工具执行失败。' });
2562
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
+ }
2563
2619
  else if (isSkillLaunchResult(event)) {
2564
2620
  badges.push({ label: '工具调用成功', className: 'metric-tool-success', title: 'Launching skill 表示 Skill 工具调用已被 runtime 接受并启动;这不是 skill 执行结束。' });
2565
2621
  }
@@ -2649,13 +2705,11 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2649
2705
  };
2650
2706
  const metricAnnotationTarget = (event, metricKey, metricScopeId) => observationMetricAnnotationTargetId({ ...event, metricScopeId }, metricKey);
2651
2707
  const metricAnnotationVerdict = (event, metricKey, metricScopeId) => {
2652
- const targetId = metricAnnotationTarget(event, metricKey, metricScopeId);
2653
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2708
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2654
2709
  return entry?.verdict === 'confirmed' || entry?.verdict === 'rejected' ? entry.verdict : '';
2655
2710
  };
2656
2711
  const metricAnnotationReason = (event, metricKey, metricScopeId) => {
2657
- const targetId = metricAnnotationTarget(event, metricKey, metricScopeId);
2658
- const entry = reviewState.entries[reviewStateKey('evidence_metric', targetId)];
2712
+ const entry = observationMetricAnnotationEntry(reviewState, { ...event, metricScopeId }, metricKey);
2659
2713
  return typeof entry?.reason === 'string' ? entry.reason : '';
2660
2714
  };
2661
2715
  const timelineManualMetricKeys = () => [
@@ -2691,10 +2745,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2691
2745
  ? '已加入窗口'
2692
2746
  : '加入窗口';
2693
2747
  const source = {
2748
+ traceId: event.traceId,
2694
2749
  sourceTrace: event.sourceTrace,
2695
2750
  sessionId: event.sessionId,
2696
2751
  messageIndex: event.messageIndex,
2697
2752
  messageUuid: event.messageUuid,
2753
+ callInstanceId: event.callInstanceId,
2698
2754
  toolUseId: event.toolUseId,
2699
2755
  snippet: (event.snippet ?? '').slice(0, 240),
2700
2756
  };
@@ -2706,10 +2762,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2706
2762
  data-manual-mark-goal-action="${e(goalAction)}"
2707
2763
  data-manual-mark-metrics="${e(JSON.stringify(metrics))}"
2708
2764
  data-manual-mark-source="${e(JSON.stringify(source))}"
2765
+ data-trace-id="${e(event.traceId ?? '')}"
2709
2766
  data-source-trace="${e(event.sourceTrace)}"
2710
2767
  data-session-id="${e(event.sessionId)}"
2711
2768
  data-message-index="${event.messageIndex ?? ''}"
2712
2769
  data-message-uuid="${e(event.messageUuid ?? '')}"
2770
+ data-call-instance-id="${e(event.callInstanceId ?? '')}"
2713
2771
  data-tool-use-id="${e(event.toolUseId ?? '')}"
2714
2772
  data-snippet="${e((event.snippet ?? '').slice(0, 240))}"
2715
2773
  onclick="openTimelineManualMark(this)"
@@ -2732,16 +2790,6 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2732
2790
  const isInsideSkillWindow = (event) => {
2733
2791
  if (options.activeEventIds)
2734
2792
  return options.activeEventIds.has(event.id);
2735
- if (options.activeSourceTrace && event.sourceTrace !== options.activeSourceTrace)
2736
- return false;
2737
- if (typeof event.messageIndex !== 'number')
2738
- return true;
2739
- const start = options.activeStartRecordIndex;
2740
- const end = options.activeEndRecordIndex;
2741
- if (typeof start === 'number' && event.messageIndex < start)
2742
- return false;
2743
- if (typeof end === 'number' && event.messageIndex > end)
2744
- return false;
2745
2793
  return true;
2746
2794
  };
2747
2795
  const isAddedToSkillWindow = (event) => goalSliceCorrectionAction(reviewSessionId, event) === 'add_to_current_skill_window';
@@ -2880,16 +2928,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2880
2928
  </div>`;
2881
2929
  };
2882
2930
  const renderSessionTimelineTree = (session) => {
2883
- 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));
2884
2938
  const fullTimelineOptions = {
2885
2939
  reviewSessionId: session.id,
2886
2940
  currentSkillName: session.skillName,
2887
2941
  includeDownstreamFeedback: shouldIncludeDownstreamFeedbackForDisplay(session),
2888
2942
  feedbackSignals: session.sessionStory?.episodes?.flatMap((episode) => episode.feedbackSignals ?? []) ?? [],
2889
2943
  activeEventIds,
2890
- activeSourceTrace: session.sourceTrace,
2891
- activeStartRecordIndex: session.timelineScope?.segmentStartRecordIndex,
2892
- activeEndRecordIndex: session.timelineScope?.segmentEndRecordIndex,
2893
2944
  showSkillWindowMarkers: true,
2894
2945
  suppressMetricTagsOutsideSkillWindow: true,
2895
2946
  };
@@ -2926,7 +2977,14 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2926
2977
  </section>
2927
2978
  </div>`;
2928
2979
  };
2929
- 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
+ };
2930
2988
  const renderTimelineScopeNotice = (session) => {
2931
2989
  const scope = session.timelineScope;
2932
2990
  if (!scope) {
@@ -2939,17 +2997,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
2939
2997
  <button type="button" disabled title="当前 report JSON 没有 fullSessionTimeline 字段,无法在前端还原完整 session。">需要重新生成报告</button>
2940
2998
  </div>`;
2941
2999
  }
2942
- const truncatedText = scope.truncated ? '当前 skill 窗口不是完整 session 链路' : '当前展示覆盖本次 skill 窗口';
2943
- const omittedText = `前面省略 ${scope.omittedBeforeCount} 个事件,后面省略 ${scope.omittedAfterCount} 个事件`;
3000
+ const truncatedText = scope.truncated ? '当前 Skill 事件窗口不是完整 session 链路' : '当前展示覆盖本次 Skill 事件窗口';
3001
+ const omittedText = `规范时间线中,窗口前有 ${scope.omittedBeforeCount} 个事件,窗口后有 ${scope.omittedAfterCount} 个事件`;
2944
3002
  const branchCount = session.timelineTree?.branches.length ?? 0;
2945
3003
  const chainText = branchCount > 0
2946
- ? `链路结构:主线 main + ${branchCount} 条 subagent 子链路;# 编号是各自 jsonl 文件内的 message index。`
2947
- : '链路结构:单 jsonl 时间线。';
3004
+ ? `链路结构:主线 main + ${branchCount} 条 subagent 子链路;record 编号只在各自物理 trace 内有效。`
3005
+ : '链路结构:单物理 trace 时间线。';
2948
3006
  return `<div class="timeline-scope-notice" data-timeline-scope-notice>
2949
3007
  <div>
2950
3008
  <strong>${e(truncatedText)}</strong>
2951
- <span>当前 skill 窗口事件:${scope.previewEventCount} 条 / 完整 session ${scope.fullSessionEventCount} 条事件</span>
2952
- <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>
2953
3013
  <span>${e(chainText)}</span>
2954
3014
  </div>
2955
3015
  <button type="button" data-full-session-toggle onclick="toggleFullSessionTimeline(this)">查看完整 session 时间线</button>
@@ -3020,7 +3080,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3020
3080
  <td style="padding:8px 10px">${renderSignalLabel(item)}</td>
3021
3081
  <td class="num" style="padding:8px 10px;text-align:right">${renderOccurrences(item)}</td>
3022
3082
  <td class="num" style="padding:8px 10px;text-align:right">${item.confidence.toFixed(2)} / ${item.attributionConfidence.toFixed(2)}</td>
3023
- <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>
3024
3084
  <td style="padding:8px 10px">${renderEvidenceCell(item)}</td>
3025
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>
3026
3086
  </tr>
@@ -3039,8 +3099,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3039
3099
  ${renderField('signalMeaning', `原始信号=${item.signalType};OMK 判断出的失败原因=${item.signalSubtype}。例如 failed_search 表示工具搜索/读取失败,bash_probe/not_found/tool_limit 是 OMK 根据命令和输出内容进一步判断出来的原因。`)}
3040
3100
  ${renderField('occurrencesMeaning', `出现次数=${item.occurrences};按 skill/cwd/signal/subtype/query/path 聚合去重后的同类事件数量。`)}
3041
3101
  ${renderField('reviewDecision', reviewSeverityMeta(item).decision)}
3042
- ${renderField('firstSeen', item.firstSeen)}
3043
- ${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}`)}
3044
3105
  ${renderField('recentSessionIds', item.recentSessionIds.join(', '))}
3045
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>
3046
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>
@@ -3091,7 +3152,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3091
3152
  low: groupItems.filter((item) => item.severity === 'low').length,
3092
3153
  noise: groupItems.filter((item) => item.severity === 'noise').length,
3093
3154
  };
3094
- 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, '');
3095
3158
  const latest = latestObservation || skillInvocationLastSeen[skillName] || '';
3096
3159
  const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
3097
3160
  const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
@@ -3135,7 +3198,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3135
3198
  <span style="font-family:ui-monospace,monospace;font-size:14px;font-weight:700">${e(skillName)}</span>
3136
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>
3137
3200
  </div>
3138
- <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>
3139
3202
  </div>
3140
3203
  <div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">
3141
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>
@@ -3167,7 +3230,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3167
3230
  };
3168
3231
  const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
3169
3232
  const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
3170
- 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, '');
3171
3236
  const lastUsed = skillInvocationLastSeen[skillName] || lastProblemSeen || '';
3172
3237
  const sources = Array.from(new Set(groupItems.map((item) => item.sourceKind))).sort();
3173
3238
  const observationCount = groupItems.length;
@@ -3496,7 +3561,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3496
3561
  <div>触发判断:${e(attributionText)}</div>
3497
3562
  ${sourceMetadataHtml}
3498
3563
  </td>
3499
- <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>
3500
3567
  </tr>
3501
3568
  <tr id="${e(chainId)}" data-context-chain-template style="display:none">
3502
3569
  <td colspan="8">${renderSkillChainTemplate(skill.skillName)}</td>
@@ -3539,11 +3606,15 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3539
3606
  ${metric('自我纠正', indicators.selfCorrectionCount ?? 0, 'selfCorrection')} ·
3540
3607
  ${metric('重复执行', indicators.repeatedExecutionCount ?? 0, 'repeatedExecution')} ·
3541
3608
  ${metric('工具执行失败', indicators.toolFailureCount, 'toolFailure')} ·
3609
+ ${metric('工具调用取消', indicators.toolCancelledCount ?? 0, 'toolCancelled')} ·
3610
+ ${metric('工具状态未知', indicators.toolUnknownCount ?? 0, 'toolUnknown')} ·
3542
3611
  ${metric('高优先级过程发现', indicators.highObservationCount, 'highObservation')}
3543
3612
  </td>
3544
3613
  <td class="session-time-cell" style="padding:9px 10px;color:var(--text-muted);font-size:12px;white-space:normal">
3545
- <div>${e(formatTimeRange(session.sourceSessionStartTimestamp ?? session.startTimestamp, session.sourceSessionEndTimestamp ?? session.endTimestamp, session.sourceSessionDurationMs))}</div>
3546
- <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>
3547
3618
  </td>
3548
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>
3549
3620
  </tr>
@@ -3570,7 +3641,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3570
3641
  <div style="font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:8px">这块只根据固定规则给“是否值得先看”的建议,例如优先复盘、抽样确认、上下文不足。它不是“符合预期/不符合预期”的最终结论。</div>
3571
3642
  ${renderAssistiveInference(shownInference)}
3572
3643
  <div style="font-size:12px;color:var(--text-secondary);line-height:1.55">
3573
- <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>
3574
3645
  <div>复盘分数:${session.reviewPriorityScore}</div>
3575
3646
  <div>关联 invocation:${session.invocationIds.length}</div>
3576
3647
  <div>关联过程发现:${session.relatedObservationIds.length}</div>
@@ -3602,22 +3673,27 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3602
3673
  }, new Map()).entries())
3603
3674
  .sort((a, b) => (sessionSkillOrder.get(a[0]) ?? Number.MAX_SAFE_INTEGER) - (sessionSkillOrder.get(b[0]) ?? Number.MAX_SAFE_INTEGER));
3604
3675
  const experienceSessionGroupsHtml = experienceSessionGroups.map(([skillName, sessions], groupIndex) => {
3605
- 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, '');
3606
3678
  const earliestSessionStart = sessions.reduce((min, session) => {
3607
- const value = session.sourceSessionStartTimestamp ?? session.startTimestamp;
3679
+ const value = session.sourceSessionStartTimestamp
3680
+ ?? (sessionTimestampedInvocationCount(session) > 0 ? session.startTimestamp : '');
3608
3681
  return value && value < min ? value : min;
3609
- }, sessions[0]?.sourceSessionStartTimestamp ?? sessions[0]?.startTimestamp ?? '');
3682
+ }, '');
3610
3683
  const latestSessionEnd = sessions.reduce((max, session) => {
3611
- const value = session.sourceSessionEndTimestamp ?? session.endTimestamp;
3684
+ const value = session.sourceSessionEndTimestamp
3685
+ ?? (sessionTimestampedInvocationCount(session) > 0 ? session.endTimestamp : '');
3612
3686
  return value && value > max ? value : max;
3613
- }, sessions[0]?.sourceSessionEndTimestamp ?? sessions[0]?.endTimestamp ?? '');
3687
+ }, '');
3614
3688
  const reviewFirst = sessions.filter((session) => inboxResolvedPriority(session) === 'review_first').length;
3615
3689
  const sampleReview = sessions.filter((session) => inboxResolvedPriority(session) === 'sample_review').length;
3616
3690
  return `<details id="${e(experienceSkillAnchor(skillName))}" class="experience-session-group" open style="scroll-margin-top:16px">
3617
3691
  <summary>
3618
3692
  <div>
3619
3693
  <span class="experience-session-skill">${e(skillName)}</span>
3620
- <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>
3621
3697
  </div>
3622
3698
  <div class="experience-session-tags">
3623
3699
  <span style="color:var(--red)">优先复盘 ${reviewFirst}</span>
@@ -3646,16 +3722,6 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3646
3722
  </details>`;
3647
3723
  }).join('');
3648
3724
  void experienceSessionGroupsHtml;
3649
- const experienceIndicators = experience?.sessions.reduce((acc, session) => {
3650
- const indicators = displayIndicatorsForSession(session);
3651
- return {
3652
- toolCallCount: acc.toolCallCount + indicators.toolCallCount,
3653
- toolFailureCount: acc.toolFailureCount + indicators.toolFailureCount,
3654
- userInterruptionCount: acc.userInterruptionCount + indicators.userInterruptionCount,
3655
- };
3656
- }, { toolCallCount: 0, toolFailureCount: 0, userInterruptionCount: 0 }) ?? { toolCallCount: 0, toolFailureCount: 0, userInterruptionCount: 0 };
3657
- const experienceToolSuccessCount = Math.max(0, experienceIndicators.toolCallCount - experienceIndicators.toolFailureCount);
3658
- void experienceToolSuccessCount;
3659
3725
  const experienceReviewSkillCount = experience?.skills.filter((skill) => skill.reviewFirstSessionCount + skill.sampleReviewSessionCount > 0).length ?? 0;
3660
3726
  void experienceReviewSkillCount;
3661
3727
  const experienceReviewSessionCount = experience?.sessions.filter((session) => inboxResolvedPriority(session) !== 'routine_sample').length ?? 0;
@@ -3706,7 +3772,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3706
3772
  inboxSiblingsMap.set(skill.sessionId, arr);
3707
3773
  }
3708
3774
  for (const arr of inboxSiblingsMap.values()) {
3709
- 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));
3710
3777
  }
3711
3778
  const inboxSkillsMap = new Map();
3712
3779
  for (const s of inboxSessions) {
@@ -3716,13 +3783,16 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3716
3783
  }
3717
3784
  const inboxPriorityRank = (priority) => priority === 'review_first' ? 0 : priority === 'sample_review' ? 1 : 2;
3718
3785
  const inboxSkillCards = Array.from(inboxSkillsMap.entries()).map(([skillName, sessions]) => {
3719
- 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));
3720
3788
  const priority = sessions.find((s) => inboxResolvedPriority(s) === 'review_first')
3721
3789
  ? 'review_first'
3722
3790
  : sessions.find((s) => inboxResolvedPriority(s) === 'sample_review')
3723
3791
  ? 'sample_review'
3724
3792
  : 'routine_sample';
3725
- 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, '');
3726
3796
  return { skillName, sessions, priority, latestEnd };
3727
3797
  });
3728
3798
  inboxSkillCards.sort((a, b) => inboxPriorityRank(a.priority) - inboxPriorityRank(b.priority) || b.latestEnd.localeCompare(a.latestEnd) || a.skillName.localeCompare(b.skillName));
@@ -3882,7 +3952,9 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
3882
3952
  };
3883
3953
  const inboxEntrypointShort = (session) => formatEntrypoint(session.entrypoint) || '未记录';
3884
3954
  const inboxFormatSessionLabel = (session) => {
3885
- 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
+ : '';
3886
3958
  const entrypoint = inboxEntrypointShort(session);
3887
3959
  const sessionShort = session.sessionId.length > 10 ? `${session.sessionId.slice(0, 8)}…` : session.sessionId;
3888
3960
  return [`Session ${sessionShort}`, entrypoint !== '未记录' ? entrypoint : '', time].filter(Boolean).join(' · ') || 'Session 调用记录';
@@ -4002,8 +4074,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4002
4074
  const roleLabel = link ? inboxSkillRoleLabel(link.role) : '执行';
4003
4075
  const roleHelpKey = inboxSkillRoleHelpKey(link?.role ?? 'executor');
4004
4076
  const indicators = displayIndicatorsForSession(skill);
4005
- const dur = formatTimeRange(skill.startTimestamp, skill.endTimestamp);
4006
- 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
+ : '未记录';
4007
4081
  const resolvedPriority = inboxResolvedPriority(skill);
4008
4082
  const priorityLabel = resolvedPriority === 'review_first' ? '要看一眼' : resolvedPriority === 'sample_review' ? '抽样' : '常规';
4009
4083
  const priorityCls = resolvedPriority === 'review_first' ? 'is-priority-high' : resolvedPriority === 'sample_review' ? 'is-priority-medium' : 'is-priority-low';
@@ -4024,6 +4098,8 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4024
4098
  <span>调用段 ${skill.invocationIds.length}</span>
4025
4099
  <span>工具 ${indicators.toolCallCount}</span>
4026
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>` : ''}
4027
4103
  </div>
4028
4104
  <div class="inbox-flow-range">${e(dur)}</div>
4029
4105
  </div>
@@ -4776,7 +4852,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4776
4852
  const out = {};
4777
4853
  for (const inv of inboxGetSessionInvocations(session)) {
4778
4854
  for (const [k, v] of Object.entries(inv.toolCounts ?? {}))
4779
- out[k] = (out[k] ?? 0) + v;
4855
+ incrementRecordCount(out, k, v);
4780
4856
  }
4781
4857
  return out;
4782
4858
  };
@@ -4792,7 +4868,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4792
4868
  for (const inv of invocations) {
4793
4869
  for (const event of inv.timeline) {
4794
4870
  if (event.kind === 'tool_result' && event.isError && event.toolName) {
4795
- failures[event.toolName] = (failures[event.toolName] ?? 0) + 1;
4871
+ incrementRecordCount(failures, event.toolName);
4796
4872
  }
4797
4873
  }
4798
4874
  }
@@ -4958,15 +5034,19 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4958
5034
  skill.sourceMetadata?.model,
4959
5035
  skill.sourceMetadata?.modelApi,
4960
5036
  ].filter(Boolean).join(' / ') || '未记录模型';
4961
- const tokenDetail = metrics?.tokenUsage ? [
4962
- { name: '输入 token', count: metrics.tokenUsage.inputTokens },
4963
- { name: '输出 token', count: metrics.tokenUsage.outputTokens },
4964
- { name: 'cache 读取', count: metrics.tokenUsage.cacheReadTokens },
4965
- { 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 },
4966
5044
  ] : [];
4967
5045
  const cards = !metrics ? [] : [
4968
5046
  { key: 'toolCall', label: '工具调用', value: indicators.toolCallCount, detail: toolDetail, note: '本次能力调用段内触发的工具调用总数及各类工具的命中分布。', anomaly: false },
4969
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 },
4970
5050
  { key: 'userMessage', label: '用户消息', value: indicators.userMessageCount, detail: [], note: '本次能力调用段内的真实人工用户消息条数(已剔除 Skill 文档注入和运行时注入)。', anomaly: false },
4971
5051
  { key: 'userFollowUp', label: '追问', value: indicators.userFollowUpCount, detail: [], note: '按当前 skill 的归因结果统计用户追问 / 补充;点击版块 ④ 可用同名标签定位原文。', anomaly: indicators.userFollowUpCount > 0 },
4972
5052
  { key: 'userCorrection', label: '纠正', value: indicators.userCorrectionCount, detail: [], note: '用户明确纠正、否决前一轮交付的次数。出现即建议进版块 ④ 看上下文。', anomaly: indicators.userCorrectionCount > 0 },
@@ -4977,8 +5057,10 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
4977
5057
  { key: 'progress', label: '过程进展', value: metrics.assistantProgressUpdateCount ?? 0, detail: [], note: '回答中出现"正在 / 仍在 / 进度更新"等过程进展信号的次数。这类不算最终交付。', anomaly: false },
4978
5058
  { key: 'selfCorrection', label: '自我纠正', value: metrics.selfCorrectionCount ?? indicators.selfCorrectionCount ?? 0, detail: [], note: 'agent 在没有用户介入的情况下发现问题并主动修正执行策略。少量说明有恢复能力,高频说明流程不稳。', anomaly: (metrics.selfCorrectionCount ?? indicators.selfCorrectionCount ?? 0) > 0 },
4979
5059
  { key: 'repeatedExecution', label: '重复执行', value: metrics.repeatedExecutionCount ?? indicators.repeatedExecutionCount ?? 0, detail: [], note: '同类步骤、工具或流程被重复执行。高频出现时通常对应绕路或 workflow 不清晰。', anomaly: (metrics.repeatedExecutionCount ?? indicators.repeatedExecutionCount ?? 0) > 0 },
4980
- { key: 'tokenInput', label: '输入 token', value: metrics.tokenUsage?.inputTokens ?? 0, detail: tokenDetail, note: '按本次能力调用段累计的 token 用量。', anomaly: false },
4981
- { 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
+ ] : []),
4982
5064
  ];
4983
5065
  const metricRow = cards.length === 0
4984
5066
  ? `<div id="inbox-sec-runtime-metrics-${e(skill.id)}"><p class="inbox-skill-empty">这条能力调用没有运行指标。</p></div>`
@@ -5043,11 +5125,22 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5043
5125
  }
5044
5126
  if (indicators.toolFailureCount > 0)
5045
5127
  runtimeIssues.push(`工具失败 ${indicators.toolFailureCount} 次`);
5046
- const runtimeSummaryText = runtimeIssues.length > 0 ? runtimeIssues.join(' · ') : '运行指标无异常';
5047
- 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';
5048
5141
  const flowSummaryText = siblings.length > 1 ? `${siblings.length} 个能力调用段` : '单一能力调用段';
5049
5142
  const flowTemplateId = `inbox-flow-template-${safeId}`;
5050
- const evidenceSummaryText = `工具 ${indicators.toolCallCount} · 失败 ${indicators.toolFailureCount} · 用户消息 ${indicators.userMessageCount}`;
5143
+ const evidenceSummaryText = `工具 ${indicators.toolCallCount} · 失败 ${indicators.toolFailureCount}${cancelledToolOutcomes > 0 ? ` · 取消 ${cancelledToolOutcomes}` : ''}${unknownToolOutcomes > 0 ? ` · 状态未知 ${unknownToolOutcomes}` : ''} · 用户消息 ${indicators.userMessageCount}`;
5051
5144
  const navItems = [
5052
5145
  { id: `inbox-sec-completion-${safeId}`, label: '① 这次跑得怎么样' },
5053
5146
  { id: `inbox-sec-log-chain-${safeId}`, label: '② 日志上下游链路' },
@@ -5167,6 +5260,22 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5167
5260
  const empty = items.length === 0 && !experience
5168
5261
  ? `<p style="color:var(--text-muted);margin-top:24px">${activeSkill ? `当前 skill 没有可展示的调用或过程发现:${e(activeSkill)}` : (lang === 'zh' ? '暂无 inbox item。运行 omk observe ingest <sessions-dir> 生成。' : 'No inbox items. Run omk observe ingest <sessions-dir> first.')}</p>`
5169
5262
  : '';
5263
+ const recyclableObservationCount = allItems.filter((item) => item.severity !== 'noise').reduce((sum, item) => sum + item.occurrences, 0);
5264
+ const sampleFromTracesBaseCommand = `omk sample --from-traces --observations-dir ${shellQuoteArg(observationsDir || '.omk/observe-inbox')}`;
5265
+ const sampleFromTracesCommand = activeSkill
5266
+ ? `${sampleFromTracesBaseCommand} --skill ${shellQuoteArg(activeSkill)}`
5267
+ : sampleFromTracesBaseCommand;
5268
+ const observeLoopCta = recyclableObservationCount > 0
5269
+ ? `<section data-observe-feedback-loop style="margin-top:14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-muted);padding:12px 14px;font-size:13px;line-height:1.55">
5270
+ <div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap">
5271
+ <div>
5272
+ <div style="font-weight:700;color:var(--text-primary)">${lang === 'zh' ? '把已确认的 observe gap 回流成 eval sample' : 'Recycle confirmed observe gaps into eval samples'}</div>
5273
+ <div style="color:var(--text-muted);margin-top:3px">${lang === 'zh' ? `当前有 ${recyclableObservationCount} 个非噪声信号。先 review 高风险或抽样信号,确认可复现后生成草稿。` : `${recyclableObservationCount} non-noise signal(s) are available. Review high-risk / sampled signals first, then draft reproducible cases.`}</div>
5274
+ </div>
5275
+ <code style="display:block;max-width:100%;overflow:auto;white-space:nowrap;padding:6px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);color:var(--text-primary)">${e(sampleFromTracesCommand)}</code>
5276
+ </div>
5277
+ </section>`
5278
+ : '';
5170
5279
  const v0SummarySection = `
5171
5280
  <section class="observe-summary-grid" style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px">
5172
5281
  <div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
@@ -5193,6 +5302,25 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5193
5302
  <div style="color:var(--text-muted);font-size:12px">当前只展示最新一次 ingest 的结果</div>
5194
5303
  </div>
5195
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
+ : '';
5196
5324
  return layout(pageTitle, `
5197
5325
  <main class="observe-report-root">
5198
5326
  <nav style="margin-bottom:12px"><a href="/observe-health" style="color:var(--accent);text-decoration:none">${lang === 'zh' ? '能力健康度日报' : 'Skill health reports'}</a></nav>
@@ -5203,6 +5331,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5203
5331
  </div>
5204
5332
  ${activeSkill ? `<a href="/observe-inbox" style="color:var(--accent);text-decoration:none;font-size:13px">查看全量</a>` : ''}
5205
5333
  </div>
5334
+ ${ingestionNotice}
5206
5335
  <style>${OBSERVATION_INBOX_STYLES}</style>
5207
5336
  <div id="signal-global-tooltip" role="tooltip"></div>
5208
5337
  <div id="timeline-fulltext-tooltip" role="dialog" aria-modal="true" aria-hidden="true" aria-label="时间线消息详情"></div>
@@ -5222,6 +5351,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5222
5351
  <div class="metric-guide-body">${metricGuideHtml}</div>
5223
5352
  </aside>
5224
5353
  ${empty}
5354
+ ${observeLoopCta}
5225
5355
  ${experienceSection}
5226
5356
  <div data-v0-observation-view style="display:none">
5227
5357
  <div class="report-version-divider" aria-label="1.0 和 2.0 报告分隔">
@@ -6567,10 +6697,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6567
6697
  targetId: targetId,
6568
6698
  verdict: 'reviewed',
6569
6699
  note: action,
6700
+ traceId: btn ? btn.getAttribute('data-trace-id') || undefined : undefined,
6570
6701
  sourceTrace: btn ? btn.getAttribute('data-source-trace') || undefined : undefined,
6571
6702
  sessionId: btn ? btn.getAttribute('data-session-id') || undefined : undefined,
6572
6703
  messageIndex: btn && btn.getAttribute('data-message-index') ? Number(btn.getAttribute('data-message-index')) : undefined,
6573
6704
  messageUuid: btn ? btn.getAttribute('data-message-uuid') || undefined : undefined,
6705
+ callInstanceId: btn ? btn.getAttribute('data-call-instance-id') || undefined : undefined,
6574
6706
  toolUseId: btn ? btn.getAttribute('data-tool-use-id') || undefined : undefined,
6575
6707
  snippet: btn ? btn.getAttribute('data-snippet') || undefined : undefined
6576
6708
  })
@@ -6703,10 +6835,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6703
6835
  verdict: next,
6704
6836
  metricKey: metric.metricKey,
6705
6837
  metricScopeId: metric.metricScopeId || undefined,
6838
+ traceId: source.traceId || undefined,
6706
6839
  sourceTrace: source.sourceTrace || undefined,
6707
6840
  sessionId: source.sessionId || undefined,
6708
6841
  messageIndex: source.messageIndex === undefined ? undefined : Number(source.messageIndex),
6709
6842
  messageUuid: source.messageUuid || undefined,
6843
+ callInstanceId: source.callInstanceId || undefined,
6710
6844
  toolUseId: source.toolUseId || undefined,
6711
6845
  snippet: source.snippet || undefined
6712
6846
  })
@@ -6833,10 +6967,12 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
6833
6967
  targetId: targetId,
6834
6968
  verdict: next,
6835
6969
  metricKey: metricKey,
6970
+ traceId: btn.getAttribute('data-trace-id') || undefined,
6836
6971
  sourceTrace: btn.getAttribute('data-source-trace') || undefined,
6837
6972
  sessionId: btn.getAttribute('data-session-id') || undefined,
6838
6973
  messageIndex: btn.getAttribute('data-message-index') ? Number(btn.getAttribute('data-message-index')) : undefined,
6839
6974
  messageUuid: btn.getAttribute('data-message-uuid') || undefined,
6975
+ callInstanceId: btn.getAttribute('data-call-instance-id') || undefined,
6840
6976
  toolUseId: btn.getAttribute('data-tool-use-id') || undefined,
6841
6977
  snippet: btn.getAttribute('data-snippet') || undefined,
6842
6978
  reason: reason || undefined