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,25 +1,68 @@
1
- /** Trace source loading and parsing for Claude JSONL / OpenClaw JSONL / generic markdown logs. */
2
- import { readFileSync, readdirSync, statSync } from 'node:fs';
3
- import { basename, dirname, join, relative } from 'node:path';
4
- import { extractMarkdownLogSkill } from './trace-attribution.js';
5
- import { isToolResultFailureText } from './text-signals.js';
1
+ /** Trace source loading and parsing for Claude / Codex / OpenClaw JSONL and generic markdown logs. */
2
+ import { createHash } from 'node:crypto';
3
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, } from 'node:fs';
4
+ import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
5
+ import { StringDecoder } from 'node:string_decoder';
6
+ import { isCodexGuardianRollout, isCodexJsonl, parseCodexSessionFile, } from './codex-trace-adapter.js';
7
+ import { extractMarkdownLogSkill, isClaudeBuiltinCommand, stripCommandEnvelopeText, } from './trace-attribution.js';
8
+ import { isRuntimeProtocolPromptText, isSyntheticUserMessageText, isToolResultFailureText, } from './text-signals.js';
9
+ import { normalizeToolIdentity } from '../shared/tool-identity.js';
10
+ import { correlateTraceToolEvents, createTraceId, normalizeTraceTimestamp, traceTimestampBounds, } from './trace-ir.js';
11
+ import { nonNegativeMetric, tokenCount } from '../shared/token-usage.js';
12
+ import { emptyTraceIngestionSummary, mergeTraceIngestionSummaries, } from './trace-ingestion.js';
13
+ const TRACE_READ_CHUNK_BYTES = 64 * 1024;
14
+ const MAX_JSONL_RECORD_CHARS = 32 * 1024 * 1024;
15
+ const MAX_MARKDOWN_LOG_BYTES = 64 * 1024 * 1024;
6
16
  // ---------- Load ----------
7
17
  /**
8
18
  * 加载一个目录(或单个 JSONL/agent markdown log 文件)下的所有 session。
9
19
  * 递归扫描目录,覆盖 Claude Code 主 session 旁边的 subagents/*.jsonl,
10
20
  * 也支持 agent workspace/logs/*.log 里的 Markdown 对话记录。
11
21
  */
12
- export function loadCcSessions(path) {
22
+ export function loadTraceSessions(path) {
23
+ return loadTraceCorpus(path).sessions;
24
+ }
25
+ export function loadTraceCorpus(path) {
13
26
  const stat = statSync(path);
14
27
  if (stat.isFile()) {
15
- return parseTraceFile(path).map((session) => withStandaloneTraceMetadata(session));
28
+ const parsed = parseTraceFile(path);
29
+ return {
30
+ sessions: resolveParentLinkedSessionGroups(parsed.sessions.map((session) => withStandaloneTraceMetadata(session))),
31
+ ingestion: parsed.ingestion,
32
+ };
16
33
  }
17
34
  const entries = collectTraceFiles(path);
18
- return annotateSessionGroups(path, entries.flatMap(parseTraceFile));
35
+ const parsed = entries.map(parseTraceFile);
36
+ return {
37
+ sessions: resolveParentLinkedSessionGroups(annotateSessionGroups(path, parsed.flatMap((entry) => entry.sessions))),
38
+ ingestion: mergeTraceIngestionSummaries(parsed.map((entry) => entry.ingestion)),
39
+ };
19
40
  }
20
- function collectTraceFiles(dir) {
41
+ /** @deprecated Use `loadTraceSessions`. */
42
+ export const loadCcSessions = loadTraceSessions;
43
+ function collectTraceFiles(dir, visitedDirs = new Set(), visitedFiles = new Set(), rootRealPath) {
44
+ let realDir;
45
+ try {
46
+ realDir = realpathSync(dir);
47
+ }
48
+ catch (cause) {
49
+ throw new Error(`无法读取 trace 输入目录:${dir}`, { cause });
50
+ }
51
+ const scanRoot = rootRealPath ?? realDir;
52
+ if (!isWithinTraceRoot(scanRoot, realDir))
53
+ return [];
54
+ if (visitedDirs.has(realDir))
55
+ return [];
56
+ visitedDirs.add(realDir);
21
57
  const files = [];
22
- for (const entry of readdirSync(dir)) {
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir).sort();
61
+ }
62
+ catch (cause) {
63
+ throw new Error(`无法扫描 trace 输入目录:${dir}`, { cause });
64
+ }
65
+ for (const entry of entries) {
23
66
  if (entry === 'node_modules' || entry === '.git')
24
67
  continue;
25
68
  const entryPath = join(dir, entry);
@@ -27,33 +70,71 @@ function collectTraceFiles(dir) {
27
70
  try {
28
71
  stat = statSync(entryPath);
29
72
  }
30
- catch {
31
- continue;
73
+ catch (cause) {
74
+ throw new Error(`无法读取 trace 输入项:${entryPath}`, { cause });
32
75
  }
33
76
  if (stat.isDirectory()) {
34
- files.push(...collectTraceFiles(entryPath));
77
+ files.push(...collectTraceFiles(entryPath, visitedDirs, visitedFiles, scanRoot));
35
78
  }
36
79
  else if (entry.endsWith('.jsonl') || entry.endsWith('.log')) {
80
+ let realFile;
81
+ try {
82
+ realFile = realpathSync(entryPath);
83
+ }
84
+ catch (cause) {
85
+ throw new Error(`无法解析 trace 输入文件:${entryPath}`, { cause });
86
+ }
87
+ if (!isWithinTraceRoot(scanRoot, realFile))
88
+ continue;
89
+ if (visitedFiles.has(realFile))
90
+ continue;
91
+ visitedFiles.add(realFile);
37
92
  files.push(entryPath);
38
93
  }
39
94
  }
40
95
  return files.sort();
41
96
  }
97
+ function isWithinTraceRoot(rootRealPath, candidateRealPath) {
98
+ const rel = relative(rootRealPath, candidateRealPath);
99
+ return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
100
+ }
101
+ const JSONL_TRACE_ADAPTERS = [
102
+ {
103
+ sourceKind: 'codex',
104
+ matches: isCodexJsonl,
105
+ parse: parseCodexSessionFile,
106
+ shouldFilter: isCodexGuardianRollout,
107
+ },
108
+ {
109
+ sourceKind: 'openclaw',
110
+ matches: isOpenClawJsonl,
111
+ parse: parseOpenClawSessionFile,
112
+ },
113
+ {
114
+ sourceKind: 'claude',
115
+ matches: isClaudeJsonl,
116
+ parse: parseClaudeSessionFile,
117
+ },
118
+ ];
42
119
  function parseTraceFile(filePath) {
43
- if (filePath.endsWith('.jsonl'))
44
- return [parseJsonlSessionFile(filePath)];
120
+ if (filePath.endsWith('.jsonl')) {
121
+ return parseJsonlSessionFile(filePath);
122
+ }
45
123
  if (filePath.endsWith('.log'))
46
124
  return parseMarkdownLogFile(filePath);
47
- return [];
125
+ return {
126
+ sessions: [],
127
+ ingestion: emptyTraceIngestionSummary(),
128
+ };
48
129
  }
49
130
  function withStandaloneTraceMetadata(session) {
50
131
  return {
51
132
  ...session,
52
- sessionGroupId: session.sessionId,
53
- sessionGroupPath: dirname(session.sourcePath),
54
- traceId: traceIdFor(session),
55
- traceRole: session.traceRole ?? 'standalone',
56
- traceLabel: session.traceLabel ?? basename(session.sourcePath),
133
+ rootRunId: session.rootRunId || session.runId,
134
+ groupPath: session.groupPath || dirname(session.sourcePath),
135
+ traceId: session.traceId || traceIdFor(session),
136
+ role: session.role ?? 'standalone',
137
+ label: session.label || basename(session.sourcePath),
57
138
  };
58
139
  }
59
140
  function annotateSessionGroups(rootPath, sessions) {
@@ -71,7 +152,7 @@ function annotateSessionGroups(rootPath, sessions) {
71
152
  const mainSession = sessions
72
153
  .filter((session) => groupRootForPath(session.sourcePath, groupRoots) === root && !isSubagentTrace(session.sourcePath))
73
154
  .sort((a, b) => a.sourcePath.localeCompare(b.sourcePath))[0];
74
- groupIdByRoot.set(root, mainSession?.sessionId || basename(root) || relative(dirname(rootPath), root) || root);
155
+ groupIdByRoot.set(root, mainSession?.runId || basename(root) || relative(dirname(rootPath), root) || root);
75
156
  }
76
157
  return sessions.map((session) => {
77
158
  const groupRoot = groupRootForPath(session.sourcePath, groupRoots);
@@ -80,27 +161,105 @@ function annotateSessionGroups(rootPath, sessions) {
80
161
  const role = isSubagentTrace(session.sourcePath) ? 'subagent' : 'main';
81
162
  return {
82
163
  ...session,
83
- sessionGroupId: groupIdByRoot.get(groupRoot) ?? session.sessionId,
84
- sessionGroupPath: groupRoot,
85
- traceId: traceIdFor(session),
86
- traceRole: role,
87
- traceLabel: traceLabelFor(session.sourcePath, groupRoot, role),
164
+ rootRunId: groupIdByRoot.get(groupRoot) ?? session.runId,
165
+ groupPath: groupRoot,
166
+ traceId: session.traceId || traceIdFor(session),
167
+ role,
168
+ label: traceLabelFor(session.sourcePath, groupRoot, role),
88
169
  };
89
170
  });
90
171
  }
172
+ function resolveParentLinkedSessionGroups(sessions) {
173
+ const sessionBySourceAndId = new Map();
174
+ const ambiguousSessionKeys = new Set();
175
+ for (const session of sessions) {
176
+ const key = traceRunKey(session.sourceKind, session.runId);
177
+ if (ambiguousSessionKeys.has(key))
178
+ continue;
179
+ if (sessionBySourceAndId.has(key)) {
180
+ sessionBySourceAndId.delete(key);
181
+ ambiguousSessionKeys.add(key);
182
+ continue;
183
+ }
184
+ sessionBySourceAndId.set(key, session);
185
+ }
186
+ const linkedSessionKeys = new Set();
187
+ for (const session of sessions) {
188
+ if (!session.rootRunId || session.rootRunId === session.runId)
189
+ continue;
190
+ linkedSessionKeys.add(traceRunKey(session.sourceKind, session.runId));
191
+ linkedSessionKeys.add(traceRunKey(session.sourceKind, session.rootRunId));
192
+ }
193
+ return sessions.map((session) => {
194
+ const sessionKey = traceRunKey(session.sourceKind, session.runId);
195
+ if (ambiguousSessionKeys.has(sessionKey))
196
+ return isolateAmbiguousSession(session);
197
+ if (!linkedSessionKeys.has(sessionKey))
198
+ return session;
199
+ if (!session.rootRunId || session.rootRunId === session.runId) {
200
+ return {
201
+ ...session,
202
+ rootRunId: session.runId,
203
+ groupPath: `${session.sourceKind}:${session.runId}`,
204
+ };
205
+ }
206
+ const seen = new Set([session.runId]);
207
+ let groupId = session.rootRunId || session.runId;
208
+ while (true) {
209
+ if (seen.has(groupId)) {
210
+ return isolateAmbiguousSession(session);
211
+ }
212
+ seen.add(groupId);
213
+ const parentKey = traceRunKey(session.sourceKind, groupId);
214
+ if (ambiguousSessionKeys.has(parentKey))
215
+ return isolateAmbiguousSession(session);
216
+ const parent = sessionBySourceAndId.get(parentKey);
217
+ if (!parent)
218
+ break;
219
+ const parentGroupId = parent.rootRunId || parent.runId;
220
+ if (parentGroupId === parent.runId) {
221
+ groupId = parent.runId;
222
+ break;
223
+ }
224
+ groupId = parentGroupId;
225
+ }
226
+ return {
227
+ ...session,
228
+ rootRunId: groupId,
229
+ groupPath: `${session.sourceKind}:${groupId}`,
230
+ };
231
+ });
232
+ }
233
+ function traceRunKey(sourceKind, runId) {
234
+ return `${sourceKind}\u0000${runId}`;
235
+ }
236
+ function isolateAmbiguousSession(session) {
237
+ return {
238
+ ...session,
239
+ rootRunId: session.runId,
240
+ groupPath: `${session.sourceKind}:trace:${session.traceId}`,
241
+ };
242
+ }
91
243
  function traceIdFor(session) {
92
- return `${session.sessionId}\u0000${session.sourcePath}`;
244
+ return createTraceId({
245
+ sourceKind: session.sourceKind,
246
+ runId: session.runId,
247
+ sourcePath: session.sourcePath,
248
+ });
93
249
  }
94
250
  function isSubagentTrace(filePath) {
95
- return filePath.split('/').includes('subagents');
251
+ return normalizedTracePath(filePath).split('/').includes('subagents');
96
252
  }
97
253
  function subagentGroupRoot(filePath) {
98
254
  const marker = '/subagents/';
99
- const index = filePath.indexOf(marker);
255
+ const index = normalizedTracePath(filePath).indexOf(marker);
100
256
  if (index < 0)
101
257
  return undefined;
102
258
  return filePath.slice(0, index);
103
259
  }
260
+ function normalizedTracePath(filePath) {
261
+ return filePath.replaceAll('\\', '/');
262
+ }
104
263
  function groupRootForPath(filePath, groupRoots) {
105
264
  const subagentRoot = subagentGroupRoot(filePath);
106
265
  if (subagentRoot && groupRoots.has(subagentRoot))
@@ -115,75 +274,525 @@ function traceLabelFor(filePath, groupRoot, role) {
115
274
  return role === 'subagent' ? rel : `main/${basename(filePath)}`;
116
275
  }
117
276
  function parseJsonlSessionFile(filePath) {
118
- const content = readFileSync(filePath, 'utf-8');
119
277
  const records = [];
120
- for (const line of content.split('\n')) {
121
- const trimmed = line.trim();
122
- if (!trimmed)
123
- continue;
278
+ const indexedRecords = [];
279
+ let sourceRecordCount = 0;
280
+ let malformedRecordCount = 0;
281
+ let ignoredValueCount = 0;
282
+ forEachNonEmptyUtf8Line(filePath, (trimmed) => {
283
+ sourceRecordCount += 1;
124
284
  try {
125
- records.push(JSON.parse(trimmed));
285
+ const parsed = JSON.parse(trimmed);
286
+ if (isRecordObject(parsed)) {
287
+ const record = parsed;
288
+ records.push(record);
289
+ indexedRecords.push(record);
290
+ }
291
+ else {
292
+ ignoredValueCount += 1;
293
+ indexedRecords.push(undefined);
294
+ }
126
295
  }
127
296
  catch {
297
+ malformedRecordCount += 1;
298
+ indexedRecords.push(undefined);
128
299
  // malformed line → skip. cc 有罕见的截断 record, 不让单行 fail 整个 session
129
300
  }
301
+ });
302
+ const ingestion = {
303
+ fileCount: 1,
304
+ sourceRecordCount,
305
+ parsedRecordCount: records.length,
306
+ malformedRecordCount,
307
+ ignoredValueCount,
308
+ unknownEventCount: 0,
309
+ filteredSessionCount: 0,
310
+ };
311
+ if (records.length === 0)
312
+ return { sessions: [], ingestion };
313
+ const matchingAdapters = JSONL_TRACE_ADAPTERS.filter((adapter) => adapter.matches(records));
314
+ let session;
315
+ if (matchingAdapters.length === 1) {
316
+ const adapter = matchingAdapters[0];
317
+ if (adapter.shouldFilter?.(records)) {
318
+ return {
319
+ sessions: [],
320
+ ingestion: { ...ingestion, filteredSessionCount: 1 },
321
+ };
322
+ }
323
+ session = adapter.parse(filePath, indexedRecords);
324
+ }
325
+ else {
326
+ session = parseUnknownJsonlSession(filePath, indexedRecords);
327
+ }
328
+ return {
329
+ sessions: [session],
330
+ ingestion: {
331
+ ...ingestion,
332
+ unknownEventCount: session.events.filter((event) => event.eventKind === 'unknown').length,
333
+ },
334
+ };
335
+ }
336
+ function forEachNonEmptyUtf8Line(filePath, visit) {
337
+ let fd;
338
+ try {
339
+ fd = openSync(filePath, 'r');
340
+ }
341
+ catch (cause) {
342
+ throw new Error(`无法读取 trace 输入文件:${filePath}`, { cause });
343
+ }
344
+ const decoder = new StringDecoder('utf8');
345
+ const buffer = Buffer.allocUnsafe(TRACE_READ_CHUNK_BYTES);
346
+ let pending = '';
347
+ const consumeCompleteLines = () => {
348
+ let newline = pending.indexOf('\n');
349
+ while (newline >= 0) {
350
+ if (newline > MAX_JSONL_RECORD_CHARS) {
351
+ throw new Error(`trace JSONL 单条记录超过 ${MAX_JSONL_RECORD_CHARS} 字符上限:${filePath}`);
352
+ }
353
+ const trimmed = pending.slice(0, newline).trim();
354
+ pending = pending.slice(newline + 1);
355
+ if (trimmed)
356
+ visit(trimmed);
357
+ newline = pending.indexOf('\n');
358
+ }
359
+ if (pending.length > MAX_JSONL_RECORD_CHARS) {
360
+ throw new Error(`trace JSONL 单条记录超过 ${MAX_JSONL_RECORD_CHARS} 字符上限:${filePath}`);
361
+ }
362
+ };
363
+ try {
364
+ while (true) {
365
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
366
+ if (bytesRead === 0)
367
+ break;
368
+ pending += decoder.write(buffer.subarray(0, bytesRead));
369
+ consumeCompleteLines();
370
+ }
371
+ pending += decoder.end();
372
+ consumeCompleteLines();
373
+ const trimmed = pending.trim();
374
+ if (trimmed.length > MAX_JSONL_RECORD_CHARS) {
375
+ throw new Error(`trace JSONL 单条记录超过 ${MAX_JSONL_RECORD_CHARS} 字符上限:${filePath}`);
376
+ }
377
+ if (trimmed)
378
+ visit(trimmed);
379
+ }
380
+ catch (cause) {
381
+ if (cause instanceof Error
382
+ && cause.message.startsWith('trace JSONL 单条记录超过'))
383
+ throw cause;
384
+ throw new Error(`无法解析 trace 输入文件:${filePath}`, { cause });
130
385
  }
131
- return isOpenClawJsonl(records) ? parseOpenClawSessionFile(filePath, records) : parseCcSessionFile(filePath, records);
386
+ finally {
387
+ closeSync(fd);
388
+ }
389
+ }
390
+ function isClaudeJsonl(records) {
391
+ return records.some((record) => (record.type === 'assistant' || record.type === 'user')
392
+ && typeof record.sessionId === 'string'
393
+ && isRecordObject(record.message)) || records.some((record) => isKnownClaudeRecordType(record.type)
394
+ && typeof record.sessionId === 'string');
395
+ }
396
+ function parseUnknownJsonlSession(filePath, records) {
397
+ const first = records.find((record) => Boolean(record));
398
+ const firstRecord = first;
399
+ const runId = firstRecord && typeof firstRecord.sessionId === 'string'
400
+ ? firstRecord.sessionId
401
+ : firstRecord && typeof firstRecord.runId === 'string'
402
+ ? firstRecord.runId
403
+ : basename(filePath, '.jsonl');
404
+ const events = records.flatMap((record, sourceIndex) => {
405
+ if (!record)
406
+ return [];
407
+ const rawRecord = record;
408
+ const sourceType = typeof record.type === 'string' ? record.type : 'unknown';
409
+ const sourceEventId = typeof record.uuid === 'string'
410
+ ? record.uuid
411
+ : typeof rawRecord.id === 'string'
412
+ ? rawRecord.id
413
+ : undefined;
414
+ return [{
415
+ eventKind: 'unknown',
416
+ eventId: `${runId}:${sourceIndex}:unknown`,
417
+ sourceEventId,
418
+ sourceIndex,
419
+ sourceType,
420
+ timestamp: normalizeTraceTimestamp(record.timestamp),
421
+ raw: record,
422
+ }];
423
+ });
424
+ const bounds = traceTimestampBounds(events.map((event) => event.timestamp));
425
+ return {
426
+ runId,
427
+ rootRunId: runId,
428
+ traceId: createTraceId({ sourceKind: 'unknown', runId, sourcePath: filePath }),
429
+ groupPath: dirname(filePath),
430
+ role: 'standalone',
431
+ label: basename(filePath),
432
+ sourcePath: filePath,
433
+ sourceKind: 'unknown',
434
+ events,
435
+ ...bounds,
436
+ };
132
437
  }
133
- function parseCcSessionFile(filePath, records) {
134
- const first = records.find((r) => 'sessionId' in r && typeof r.sessionId === 'string');
135
- const last = [...records].reverse().find((r) => 'timestamp' in r && typeof r.timestamp === 'string');
438
+ function parseClaudeSessionFile(filePath, records) {
439
+ const first = records.find((r) => r && 'sessionId' in r && typeof r.sessionId === 'string');
440
+ const runId = first?.sessionId ?? basename(filePath, '.jsonl');
441
+ const events = correlateTraceToolEvents(records.flatMap((record, sourceIndex) => record ? legacyRecordToTraceEvents(record, runId, sourceIndex, 'claude') : []));
442
+ const bounds = traceTimestampBounds([
443
+ ...events.map((event) => event.timestamp),
444
+ first?.timestamp,
445
+ ]);
136
446
  return {
137
- sessionId: first?.sessionId ?? filePath.split('/').pop().replace('.jsonl', ''),
447
+ runId,
448
+ rootRunId: runId,
449
+ traceId: createTraceId({ sourceKind: 'claude', runId, sourcePath: filePath }),
450
+ groupPath: dirname(filePath),
451
+ role: 'standalone',
452
+ label: basename(filePath),
138
453
  sourcePath: filePath,
139
454
  sourceKind: 'claude',
140
- records,
455
+ events,
141
456
  cwd: first?.cwd,
142
457
  gitBranch: first?.gitBranch,
143
458
  entrypoint: first?.entrypoint,
144
- startTimestamp: first?.timestamp,
145
- endTimestamp: last?.timestamp,
459
+ ...bounds,
146
460
  };
147
461
  }
462
+ export function legacyCcSessionToTraceSession(session) {
463
+ const runId = session.sessionId;
464
+ const rootRunId = session.sessionGroupId ?? runId;
465
+ const sourceKind = session.sourceKind ?? 'claude';
466
+ const traceId = session.traceId ?? createTraceId({
467
+ sourceKind,
468
+ runId,
469
+ sourcePath: session.sourcePath,
470
+ });
471
+ const events = correlateTraceToolEvents(session.records.flatMap((record, sourceIndex) => legacyRecordToTraceEvents(record, runId, sourceIndex, sourceKind)));
472
+ const bounds = traceTimestampBounds([
473
+ ...events.map((event) => event.timestamp),
474
+ session.startTimestamp,
475
+ session.endTimestamp,
476
+ ]);
477
+ return {
478
+ runId,
479
+ rootRunId,
480
+ traceId,
481
+ groupPath: session.sessionGroupPath ?? dirname(session.sourcePath),
482
+ role: session.traceRole ?? 'standalone',
483
+ label: session.traceLabel ?? basename(session.sourcePath),
484
+ sourcePath: session.sourcePath,
485
+ sourceKind,
486
+ events,
487
+ cwd: session.cwd,
488
+ gitBranch: session.gitBranch,
489
+ entrypoint: session.entrypoint,
490
+ sourceMetadata: session.sourceMetadata,
491
+ ...bounds,
492
+ };
493
+ }
494
+ function legacyRecordToTraceEvents(raw, runId, sourceIndex, sourceKind) {
495
+ if (!isRecordObject(raw))
496
+ return [];
497
+ const sourceType = typeof raw.type === 'string' ? raw.type : 'unknown';
498
+ const timestamp = normalizeTraceTimestamp(raw.timestamp);
499
+ const sourceEventId = typeof raw.uuid === 'string'
500
+ ? raw.uuid
501
+ : typeof raw.id === 'string'
502
+ ? raw.id
503
+ : undefined;
504
+ const eventId = (suffix) => `${runId}:${sourceIndex}:${suffix}`;
505
+ if (sourceType === 'user' && isRecordObject(raw.message)) {
506
+ const content = raw.message.content;
507
+ const events = [];
508
+ const parts = typeof content === 'string' ? [{ type: 'text', text: content }] : Array.isArray(content) ? content : [];
509
+ let partIndex = 0;
510
+ for (const part of parts) {
511
+ if (!isRecordObject(part))
512
+ continue;
513
+ if (part.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
514
+ const text = sourceKind === 'claude'
515
+ ? stripClaudeBuiltinCommandEnvelope(part.text)
516
+ : part.text;
517
+ if (!text) {
518
+ partIndex += 1;
519
+ continue;
520
+ }
521
+ events.push({
522
+ eventKind: 'message',
523
+ eventId: eventId(`message-${partIndex}`),
524
+ sourceEventId,
525
+ sourceIndex,
526
+ sourceType,
527
+ timestamp,
528
+ role: 'user',
529
+ origin: classifyUserMessageOrigin(raw, text),
530
+ text,
531
+ });
532
+ }
533
+ else if (part.type === 'tool_result'
534
+ && typeof part.tool_use_id === 'string') {
535
+ const output = typeof part.content === 'string' ? part.content : JSON.stringify(part.content ?? '');
536
+ const explicit = typeof part.is_error === 'boolean';
537
+ const inferredFailure = !explicit && isToolResultFailureText(output);
538
+ events.push({
539
+ eventKind: 'tool_result',
540
+ eventId: eventId(`tool-result-${partIndex}`),
541
+ sourceEventId,
542
+ sourceIndex,
543
+ sourceType,
544
+ timestamp,
545
+ callId: part.tool_use_id,
546
+ output,
547
+ status: explicit
548
+ ? part.is_error === true ? 'failure' : 'success'
549
+ : inferredFailure ? 'failure' : 'unknown',
550
+ statusSource: explicit ? 'runtime' : inferredFailure ? 'inferred' : 'unknown',
551
+ });
552
+ }
553
+ partIndex += 1;
554
+ }
555
+ return events;
556
+ }
557
+ if (sourceType === 'assistant' && isRecordObject(raw.message)) {
558
+ const events = [];
559
+ const content = Array.isArray(raw.message.content) ? raw.message.content : [];
560
+ const model = typeof raw.message.model === 'string' ? raw.message.model : undefined;
561
+ const text = content.flatMap((part) => isRecordObject(part) && part.type === 'text' && typeof part.text === 'string' ? [part.text] : []).join('\n');
562
+ if (text) {
563
+ events.push({
564
+ eventKind: 'message',
565
+ eventId: eventId('message'),
566
+ sourceEventId,
567
+ sourceIndex,
568
+ sourceType,
569
+ timestamp,
570
+ role: 'assistant',
571
+ origin: 'synthetic',
572
+ text,
573
+ model,
574
+ attributionSkill: typeof raw.attributionSkill === 'string' ? raw.attributionSkill : undefined,
575
+ });
576
+ }
577
+ content.forEach((part, partIndex) => {
578
+ if (isRecordObject(part)
579
+ && part.type === 'tool_use'
580
+ && typeof part.id === 'string'
581
+ && typeof part.name === 'string') {
582
+ events.push({
583
+ eventKind: 'tool_call',
584
+ eventId: eventId(`tool-call-${partIndex}`),
585
+ sourceEventId,
586
+ sourceIndex,
587
+ sourceType,
588
+ timestamp,
589
+ callId: part.id,
590
+ tool: normalizeToolIdentity({ sourceName: part.name }),
591
+ input: isRecordObject(part.input) ? part.input : {},
592
+ model,
593
+ });
594
+ }
595
+ });
596
+ if (isRecordObject(raw.message.usage)) {
597
+ const usageEvent = usageEventFromLegacy(raw.message.usage, eventId('usage'), sourceIndex, sourceType, timestamp, model);
598
+ events.push(usageEvent ?? {
599
+ eventKind: 'unknown',
600
+ eventId: eventId('invalid-usage'),
601
+ sourceEventId,
602
+ sourceIndex,
603
+ sourceType,
604
+ timestamp,
605
+ raw,
606
+ });
607
+ }
608
+ return events;
609
+ }
610
+ if (sourceType === 'system') {
611
+ if (raw.subtype === 'turn_duration') {
612
+ return [{
613
+ eventKind: 'lifecycle',
614
+ eventId: eventId('turn-completed'),
615
+ sourceEventId,
616
+ sourceIndex,
617
+ sourceType,
618
+ timestamp,
619
+ phase: 'turn_completed',
620
+ durationMs: nonNegativeMetric(raw.durationMs),
621
+ }];
622
+ }
623
+ const text = claudeSystemRecordText(raw);
624
+ if (text) {
625
+ return [{
626
+ eventKind: 'message',
627
+ eventId: eventId('system-message'),
628
+ sourceEventId,
629
+ sourceIndex,
630
+ sourceType,
631
+ timestamp,
632
+ role: 'system',
633
+ origin: 'runtime',
634
+ text,
635
+ }];
636
+ }
637
+ return [];
638
+ }
639
+ const lifecycle = lifecycleEventFromLegacy(raw, runId, sourceIndex, sourceType, timestamp);
640
+ if (lifecycle)
641
+ return [lifecycle];
642
+ if (isKnownClaudeRecordType(sourceType))
643
+ return [];
644
+ return [{
645
+ eventKind: 'unknown',
646
+ eventId: eventId('unknown'),
647
+ sourceEventId,
648
+ sourceIndex,
649
+ sourceType,
650
+ timestamp,
651
+ raw,
652
+ }];
653
+ }
654
+ function stripClaudeBuiltinCommandEnvelope(text) {
655
+ const match = /<command-name>\/([^<]+)<\/command-name>/.exec(text);
656
+ return match?.[1] && isClaudeBuiltinCommand(match[1])
657
+ ? stripCommandEnvelopeText(text)
658
+ : text;
659
+ }
660
+ function isKnownClaudeRecordType(value) {
661
+ return value === 'assistant'
662
+ || value === 'user'
663
+ || value === 'attachment'
664
+ || value === 'pr-link'
665
+ || value === 'mode'
666
+ || value === 'permission-mode'
667
+ || value === 'last-prompt'
668
+ || value === 'ai-title'
669
+ || value === 'agent-name'
670
+ || value === 'system'
671
+ || value === 'file-history-snapshot'
672
+ || value === 'queue-operation';
673
+ }
674
+ function claudeSystemRecordText(record) {
675
+ if (typeof record.content === 'string' && record.content.trim())
676
+ return record.content;
677
+ if (typeof record.message === 'string' && record.message.trim())
678
+ return record.message;
679
+ if (record.subtype !== 'api_error' || !isRecordObject(record.error))
680
+ return '';
681
+ if (typeof record.error.formatted === 'string' && record.error.formatted.trim()) {
682
+ return record.error.formatted;
683
+ }
684
+ return typeof record.error.message === 'string' ? record.error.message : '';
685
+ }
686
+ function usageEventFromLegacy(usage, eventId, sourceIndex, sourceType, timestamp, model) {
687
+ if (!isValidUsageCounters(usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens))
688
+ return null;
689
+ return {
690
+ eventKind: 'usage',
691
+ eventId,
692
+ sourceIndex,
693
+ sourceType,
694
+ timestamp,
695
+ model,
696
+ inputTokens: tokenCount(usage.input_tokens),
697
+ outputTokens: tokenCount(usage.output_tokens),
698
+ cacheReadTokens: tokenCount(usage.cache_read_input_tokens),
699
+ cacheCreationTokens: tokenCount(usage.cache_creation_input_tokens),
700
+ };
701
+ }
702
+ function lifecycleEventFromLegacy(raw, runId, sourceIndex, sourceType, timestamp) {
703
+ const phase = /^session[._-]started$/i.test(sourceType)
704
+ ? 'session_started'
705
+ : /^session[._-]ended$/i.test(sourceType)
706
+ ? 'session_ended'
707
+ : /^turn[._-]started$/i.test(sourceType)
708
+ ? 'turn_started'
709
+ : /^turn[._-](?:ended|completed)$/i.test(sourceType)
710
+ ? 'turn_completed'
711
+ : /^turn[._-]aborted$/i.test(sourceType)
712
+ ? 'turn_aborted'
713
+ : /^turn[._-]interrupted$/i.test(sourceType)
714
+ ? 'turn_interrupted'
715
+ : null;
716
+ if (!phase)
717
+ return null;
718
+ return {
719
+ eventKind: 'lifecycle',
720
+ eventId: `${runId}:${sourceIndex}:lifecycle`,
721
+ sourceIndex,
722
+ sourceType,
723
+ timestamp,
724
+ turnId: typeof raw.turnId === 'string' ? raw.turnId : undefined,
725
+ phase,
726
+ reason: typeof raw.reason === 'string' ? raw.reason : undefined,
727
+ durationMs: nonNegativeMetric(raw.durationMs),
728
+ };
729
+ }
730
+ function classifyUserMessageOrigin(record, text) {
731
+ if (record.isMeta === true && typeof record.sourceToolUseID === 'string')
732
+ return 'skill-context';
733
+ if (/^Base directory for this skill:\s+.+(?:\n| )#\s+[a-z0-9][\w.-]*/i.test(text))
734
+ return 'skill-context';
735
+ if (isRuntimeInjectedMessage(text))
736
+ return 'runtime';
737
+ if (record.entrypoint === 'sdk-ts'
738
+ && typeof record.promptId === 'string'
739
+ && (/^进入.+流程。当前页面已经完成本地工作区恢复/.test(text)
740
+ || /gui-workflow route/.test(text)
741
+ || /当前页面已经完成本地工作区恢复/.test(text)))
742
+ return 'runtime';
743
+ if (isSyntheticUserMessageText(text))
744
+ return 'synthetic';
745
+ return 'human';
746
+ }
747
+ function isRuntimeInjectedMessage(text) {
748
+ const trimmed = text.trimStart();
749
+ return /^Conversation info \(untrusted metadata\):\s*```json/i.test(trimmed)
750
+ || isRuntimeProtocolPromptText(trimmed)
751
+ || /^# AGENTS\.md instructions\b/i.test(trimmed)
752
+ || /^<(?:app-context|environment_context|permissions instructions|collaboration_mode|apps_instructions|plugins_instructions|skills_instructions|recommended_plugins)>/i.test(trimmed);
753
+ }
148
754
  function isOpenClawJsonl(records) {
149
755
  return records.some((record) => record.type === 'session' && typeof record.id === 'string')
150
756
  && records.some((record) => record.type === 'message' && isRecordObject(record.message));
151
757
  }
152
758
  function parseOpenClawSessionFile(filePath, rawRecords) {
153
- const sessionRecord = rawRecords.find((record) => record.type === 'session');
759
+ const sessionRecord = rawRecords.find((record) => record?.type === 'session');
154
760
  const sessionId = typeof sessionRecord?.id === 'string'
155
761
  ? sessionRecord.id
156
- : filePath.split('/').pop().replace(/\.jsonl$/, '');
762
+ : basename(filePath, '.jsonl');
157
763
  const cwd = typeof sessionRecord?.cwd === 'string' ? sessionRecord.cwd : undefined;
158
764
  const sourceMetadata = extractOpenClawSourceMetadata(rawRecords);
159
- const records = [];
160
- for (const raw of rawRecords) {
161
- const converted = convertOpenClawRecord(raw, sessionId, cwd);
162
- if (converted) {
163
- records.push(converted);
164
- }
165
- else if (isSessionBoundaryRawRecord(raw)) {
166
- records.push({ ...raw, sessionId });
167
- }
168
- }
169
- const first = records.find((record) => typeof record.timestamp === 'string');
170
- const last = [...records].reverse().find((record) => typeof record.timestamp === 'string');
765
+ const events = correlateTraceToolEvents(rawRecords.flatMap((raw, sourceIndex) => raw ? openClawRecordToTraceEvents(raw, sessionId, sourceIndex) : []));
766
+ const bounds = traceTimestampBounds([
767
+ ...events.map((event) => event.timestamp),
768
+ sessionRecord?.timestamp,
769
+ ]);
171
770
  return {
172
- sessionId,
771
+ runId: sessionId,
772
+ rootRunId: sessionId,
773
+ traceId: createTraceId({
774
+ sourceKind: 'openclaw',
775
+ runId: sessionId,
776
+ sourcePath: filePath,
777
+ }),
778
+ groupPath: dirname(filePath),
779
+ role: 'standalone',
780
+ label: basename(filePath),
173
781
  sourcePath: filePath,
174
782
  sourceKind: 'openclaw',
175
- records,
783
+ events,
176
784
  cwd,
177
785
  entrypoint: 'openclaw',
178
786
  sourceMetadata,
179
- startTimestamp: first?.timestamp ?? (typeof sessionRecord?.timestamp === 'string' ? sessionRecord.timestamp : undefined),
180
- endTimestamp: last?.timestamp,
787
+ ...bounds,
181
788
  };
182
789
  }
183
790
  function extractOpenClawSourceMetadata(rawRecords) {
184
791
  const meta = {};
185
792
  const commands = new Set();
186
793
  for (const raw of rawRecords) {
794
+ if (!raw)
795
+ continue;
187
796
  if (raw.type === 'model_change') {
188
797
  const modelChange = raw;
189
798
  if (typeof modelChange.provider === 'string')
@@ -255,84 +864,195 @@ function extractBusinessActionNames(text) {
255
864
  }
256
865
  return names;
257
866
  }
258
- function convertOpenClawRecord(raw, sessionId, cwd) {
259
- if (raw.type !== 'message')
260
- return null;
867
+ function openClawRecordToTraceEvents(raw, sessionId, sourceIndex) {
868
+ const sourceType = typeof raw.type === 'string' ? raw.type : 'unknown';
869
+ const timestamp = normalizeTraceTimestamp(raw.timestamp);
870
+ const sourceEventId = 'id' in raw && typeof raw.id === 'string' ? raw.id : undefined;
871
+ const eventId = (suffix) => `${sessionId}:${sourceIndex}:${suffix}`;
872
+ if (raw.type === 'session') {
873
+ return [{
874
+ eventKind: 'lifecycle',
875
+ eventId: eventId('session-started'),
876
+ sourceEventId,
877
+ sourceIndex,
878
+ sourceType,
879
+ timestamp,
880
+ phase: 'session_started',
881
+ }];
882
+ }
883
+ const lifecycle = lifecycleEventFromLegacy(raw, sessionId, sourceIndex, sourceType, timestamp);
884
+ if (lifecycle)
885
+ return [lifecycle];
886
+ if (isKnownOpenClawMetadataRecord(raw))
887
+ return [];
888
+ if (raw.type !== 'message') {
889
+ return [{
890
+ eventKind: 'unknown',
891
+ eventId: eventId('unknown'),
892
+ sourceEventId,
893
+ sourceIndex,
894
+ sourceType,
895
+ timestamp,
896
+ raw,
897
+ }];
898
+ }
261
899
  const messageRecord = raw;
262
900
  const message = messageRecord.message;
263
- if (!message || typeof message.role !== 'string')
264
- return null;
265
- const uuid = typeof messageRecord.id === 'string' ? messageRecord.id : `${sessionId}-${recordsafeTimestamp(messageRecord.timestamp)}`;
266
- const parentUuid = typeof messageRecord.parentId === 'string' ? messageRecord.parentId : null;
267
- const timestamp = typeof messageRecord.timestamp === 'string' ? messageRecord.timestamp : new Date().toISOString();
901
+ if (!message || typeof message.role !== 'string') {
902
+ return [{
903
+ eventKind: 'unknown',
904
+ eventId: eventId('unknown-message'),
905
+ sourceEventId,
906
+ sourceIndex,
907
+ sourceType,
908
+ timestamp,
909
+ raw,
910
+ }];
911
+ }
268
912
  if (message.role === 'user') {
269
- return {
270
- type: 'user',
271
- uuid,
272
- parentUuid,
273
- sessionId,
913
+ return splitOpenClawRuntimeMetadata(openClawContentText(message.content))
914
+ .map((text, partIndex) => ({
915
+ eventKind: 'message',
916
+ eventId: eventId(`message-${partIndex}`),
917
+ sourceEventId,
918
+ sourceIndex,
919
+ sourceType,
274
920
  timestamp,
275
- entrypoint: 'openclaw',
276
- message: {
277
- role: 'user',
278
- content: openClawTextParts(message.content),
279
- },
280
- };
921
+ role: 'user',
922
+ origin: classifyUserMessageOrigin({ entrypoint: 'openclaw' }, text),
923
+ text,
924
+ }));
281
925
  }
282
926
  if (message.role === 'assistant') {
283
- return {
284
- type: 'assistant',
285
- uuid,
286
- parentUuid,
287
- sessionId,
288
- timestamp,
289
- cwd,
290
- entrypoint: 'openclaw',
291
- message: {
927
+ const events = [];
928
+ const model = typeof message.model === 'string' ? message.model : undefined;
929
+ const content = Array.isArray(message.content) ? message.content : [];
930
+ const text = typeof message.content === 'string'
931
+ ? message.content
932
+ : content.flatMap((part) => isRecordObject(part) && part.type === 'text' && typeof part.text === 'string'
933
+ ? [part.text]
934
+ : []).join('\n');
935
+ if (text) {
936
+ events.push({
937
+ eventKind: 'message',
938
+ eventId: eventId('message'),
939
+ sourceEventId,
940
+ sourceIndex,
941
+ sourceType,
942
+ timestamp,
292
943
  role: 'assistant',
293
- model: typeof message.model === 'string' ? message.model : undefined,
294
- content: openClawAssistantContent(message.content),
295
- stop_reason: typeof message.stopReason === 'string' ? message.stopReason : undefined,
296
- usage: openClawUsage(message.usage),
297
- },
298
- };
944
+ origin: 'synthetic',
945
+ text,
946
+ model,
947
+ });
948
+ }
949
+ content.forEach((part, partIndex) => {
950
+ if (!isRecordObject(part)
951
+ || part.type !== 'toolCall'
952
+ || typeof part.id !== 'string'
953
+ || typeof part.name !== 'string')
954
+ return;
955
+ const sourceName = part.name.trim();
956
+ // OpenClaw's native shell tool is named `exec`. Keep that protocol fact in
957
+ // this adapter instead of teaching the source-neutral identity layer that
958
+ // every custom tool named `exec` is a shell.
959
+ const tool = sourceName.toLowerCase() === 'exec'
960
+ ? { name: 'Bash', sourceName }
961
+ : normalizeToolIdentity({ sourceName });
962
+ events.push({
963
+ eventKind: 'tool_call',
964
+ eventId: eventId(`tool-call-${partIndex}`),
965
+ sourceEventId,
966
+ sourceIndex,
967
+ sourceType,
968
+ timestamp,
969
+ callId: part.id,
970
+ tool,
971
+ input: normalizeOpenClawToolInput(tool.name, isRecordObject(part.arguments) ? part.arguments : {}),
972
+ model,
973
+ });
974
+ });
975
+ if (isRecordObject(message.usage)) {
976
+ const inputTokens = message.usage.input ?? message.usage.input_tokens;
977
+ const outputTokens = message.usage.output ?? message.usage.output_tokens;
978
+ const cacheReadTokens = message.usage.cacheRead ?? message.usage.cache_read_input_tokens;
979
+ const cacheCreationTokens = message.usage.cacheWrite ?? message.usage.cache_creation_input_tokens;
980
+ if (isValidUsageCounters(inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens)) {
981
+ events.push({
982
+ eventKind: 'usage',
983
+ eventId: eventId('usage'),
984
+ sourceEventId,
985
+ sourceIndex,
986
+ sourceType,
987
+ timestamp,
988
+ model,
989
+ inputTokens: tokenCount(inputTokens),
990
+ outputTokens: tokenCount(outputTokens),
991
+ cacheReadTokens: tokenCount(cacheReadTokens),
992
+ cacheCreationTokens: tokenCount(cacheCreationTokens),
993
+ });
994
+ }
995
+ else {
996
+ events.push({
997
+ eventKind: 'unknown',
998
+ eventId: eventId('invalid-usage'),
999
+ sourceEventId,
1000
+ sourceIndex,
1001
+ sourceType,
1002
+ timestamp,
1003
+ raw,
1004
+ });
1005
+ }
1006
+ }
1007
+ return events;
299
1008
  }
300
1009
  if (message.role === 'toolResult') {
301
1010
  const toolUseId = typeof message.toolCallId === 'string' ? message.toolCallId : undefined;
302
- if (!toolUseId)
303
- return null;
1011
+ if (!toolUseId) {
1012
+ return [{
1013
+ eventKind: 'unknown',
1014
+ eventId: eventId('orphan-tool-result'),
1015
+ sourceEventId,
1016
+ sourceIndex,
1017
+ sourceType,
1018
+ timestamp,
1019
+ raw,
1020
+ }];
1021
+ }
304
1022
  const content = openClawToolResultText(message.content);
305
- return {
306
- type: 'user',
307
- uuid,
308
- parentUuid,
309
- sessionId,
310
- timestamp,
311
- entrypoint: 'openclaw',
312
- message: {
313
- role: 'user',
314
- content: [{
315
- type: 'tool_result',
316
- tool_use_id: toolUseId,
317
- content,
318
- is_error: message.isError === true || isToolResultFailureText(content),
319
- }],
320
- },
321
- };
1023
+ const hasRuntimeStatus = typeof message.isError === 'boolean';
1024
+ const inferredFailure = !hasRuntimeStatus && isToolResultFailureText(content);
1025
+ return [{
1026
+ eventKind: 'tool_result',
1027
+ eventId: eventId('tool-result'),
1028
+ sourceEventId,
1029
+ sourceIndex,
1030
+ sourceType,
1031
+ timestamp,
1032
+ callId: toolUseId,
1033
+ output: content,
1034
+ status: hasRuntimeStatus
1035
+ ? message.isError === true ? 'failure' : 'success'
1036
+ : inferredFailure ? 'failure' : 'unknown',
1037
+ statusSource: hasRuntimeStatus ? 'runtime' : inferredFailure ? 'inferred' : 'unknown',
1038
+ }];
322
1039
  }
323
- return null;
324
- }
325
- function isSessionBoundaryRawRecord(raw) {
326
- const type = typeof raw.type === 'string' ? raw.type : '';
327
- return /^session[._-](?:started|ended)$/i.test(type);
328
- }
329
- function recordsafeTimestamp(value) {
330
- return typeof value === 'string' ? value.replace(/[^a-zA-Z0-9_-]/g, '') : Math.random().toString(36).slice(2);
1040
+ return [{
1041
+ eventKind: 'unknown',
1042
+ eventId: eventId('unknown-role'),
1043
+ sourceEventId,
1044
+ sourceIndex,
1045
+ sourceType,
1046
+ timestamp,
1047
+ raw,
1048
+ }];
331
1049
  }
332
- function openClawTextParts(content) {
333
- const text = openClawContentText(content);
334
- const split = splitOpenClawRuntimeMetadata(text);
335
- return split.map((part) => ({ type: 'text', text: part }));
1050
+ function isKnownOpenClawMetadataRecord(raw) {
1051
+ if (raw.type === 'model_change')
1052
+ return true;
1053
+ if (raw.type !== 'custom')
1054
+ return false;
1055
+ return raw.customType === 'model-snapshot';
336
1056
  }
337
1057
  function splitOpenClawRuntimeMetadata(text) {
338
1058
  const match = text.match(/^(Conversation info \(untrusted metadata\):\s*```json[\s\S]*?```\s*)([\s\S]*)$/);
@@ -342,71 +1062,12 @@ function splitOpenClawRuntimeMetadata(text) {
342
1062
  const rest = match[2].trim();
343
1063
  return rest ? [metadata, rest] : [metadata];
344
1064
  }
345
- function openClawAssistantContent(content) {
346
- if (!Array.isArray(content))
347
- return [];
348
- const parts = [];
349
- for (const part of content) {
350
- if (!isRecordObject(part) || typeof part.type !== 'string')
351
- continue;
352
- if (part.type === 'thinking') {
353
- parts.push({ type: 'thinking', thinking: typeof part.thinking === 'string' ? part.thinking : '' });
354
- continue;
355
- }
356
- if (part.type === 'text') {
357
- parts.push({ type: 'text', text: typeof part.text === 'string' ? part.text : '' });
358
- continue;
359
- }
360
- if (part.type === 'toolCall') {
361
- const id = typeof part.id === 'string' ? part.id : undefined;
362
- const name = typeof part.name === 'string' ? normalizeOpenClawToolName(part.name) : undefined;
363
- if (!id || !name)
364
- continue;
365
- parts.push({
366
- type: 'tool_use',
367
- id,
368
- name,
369
- input: normalizeOpenClawToolInput(name, isRecordObject(part.arguments) ? part.arguments : {}),
370
- });
371
- }
372
- }
373
- return parts;
374
- }
375
- function normalizeOpenClawToolName(name) {
376
- const normalized = name.trim();
377
- const lower = normalized.toLowerCase();
378
- if (lower === 'read')
379
- return 'Read';
380
- if (lower === 'grep' || lower === 'search')
381
- return 'Grep';
382
- // OpenClaw uses exec/run/shell labels for command execution; collapse them for tool-count comparability.
383
- if (lower === 'bash' || lower === 'shell' || lower === 'run' || lower === 'exec')
384
- return 'Bash';
385
- if (lower === 'edit')
386
- return 'Edit';
387
- if (lower === 'write')
388
- return 'Write';
389
- return normalized;
390
- }
391
1065
  function normalizeOpenClawToolInput(toolName, input) {
392
1066
  if (toolName === 'Read' && typeof input.path === 'string' && typeof input.file_path !== 'string') {
393
1067
  return { ...input, file_path: input.path };
394
1068
  }
395
1069
  return input;
396
1070
  }
397
- function openClawUsage(value) {
398
- if (!isRecordObject(value))
399
- return undefined;
400
- return {
401
- input_tokens: numberValue(value.input ?? value.input_tokens),
402
- output_tokens: numberValue(value.output ?? value.output_tokens),
403
- cache_read_input_tokens: numberValue(value.cacheRead ?? value.cache_read_input_tokens),
404
- cache_creation_input_tokens: numberValue(value.cacheWrite ?? value.cache_creation_input_tokens),
405
- };
406
- }
407
- function numberValue(value) {
408
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
409
- }
410
1071
  function openClawToolResultText(content) {
411
1072
  return openClawContentText(content);
412
1073
  }
@@ -429,53 +1090,102 @@ function openClawContentText(content) {
429
1090
  function isRecordObject(value) {
430
1091
  return !!value && typeof value === 'object' && !Array.isArray(value);
431
1092
  }
1093
+ function isValidUsageCounters(input, output, cacheRead, cacheCreation) {
1094
+ return isTokenCounter(input)
1095
+ && isTokenCounter(output)
1096
+ && (cacheRead === undefined || isTokenCounter(cacheRead))
1097
+ && (cacheCreation === undefined || isTokenCounter(cacheCreation));
1098
+ }
1099
+ function isTokenCounter(value) {
1100
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
1101
+ }
432
1102
  const MARKDOWN_LOG_BLOCK_RE = /(?:^|\n)---\s*\n## \[([^\]]+)\] 对话记录[^\n]*\n([\s\S]*?)(?=\n---\s*\n## \[|$)/g;
433
1103
  function parseMarkdownLogFile(filePath) {
1104
+ const fileSize = statSync(filePath).size;
1105
+ if (fileSize > MAX_MARKDOWN_LOG_BYTES) {
1106
+ throw new Error(`trace Markdown 日志超过 ${MAX_MARKDOWN_LOG_BYTES} 字节上限:${filePath}`);
1107
+ }
434
1108
  const content = readFileSync(filePath, 'utf-8');
435
- if (!content.includes('### 用户输入') || !content.includes('### AI 回复'))
436
- return [];
1109
+ const sourceRecordCount = (content.match(/^## \[[^\]]+\] 对话记录.*$/gm) ?? []).length;
1110
+ if (!content.includes('### 用户输入') || !content.includes('### AI 回复')) {
1111
+ return {
1112
+ sessions: [],
1113
+ ingestion: {
1114
+ ...emptyTraceIngestionSummary(1),
1115
+ sourceRecordCount,
1116
+ malformedRecordCount: sourceRecordCount,
1117
+ },
1118
+ };
1119
+ }
437
1120
  const sessions = [];
1121
+ const streamOccurrences = new Map();
438
1122
  let index = 0;
439
1123
  for (const match of content.matchAll(MARKDOWN_LOG_BLOCK_RE)) {
440
1124
  const timestamp = markdownLogTimestampToIso(match[1]);
441
1125
  const body = match[2] ?? '';
442
1126
  const blockCwd = body.match(/^\*\*工作目录\*\*:\s*(.+)$/m)?.[1]?.trim();
443
1127
  const blockSessionId = body.match(/^\*\*会话 ID\*\*:\s*(.+)$/m)?.[1]?.trim();
444
- const requestId = body.match(/^\*\*请求 ID\*\*:\s*(.+)$/m)?.[1]?.trim() ?? String(index);
1128
+ const explicitRequestId = body.match(/^\*\*请求 ID\*\*:\s*(.+)$/m)?.[1]?.trim();
445
1129
  const userText = extractMarkdownLogSection(body, '### 用户输入', '### AI 回复');
446
1130
  const assistantText = extractMarkdownLogSection(body, '### AI 回复');
447
1131
  if (!userText && !assistantText)
448
1132
  continue;
449
- const sessionId = blockSessionId || `${filePath.split('/').pop().replace(/\.log$/, '')}:${requestId}`;
1133
+ const contentFingerprint = createHash('sha256')
1134
+ .update(`${userText}\u0000${assistantText}`)
1135
+ .digest('hex')
1136
+ .slice(0, 24);
1137
+ const requestId = explicitRequestId ?? contentFingerprint;
1138
+ const sessionId = blockSessionId || `${basename(filePath, '.log')}:${requestId}`;
1139
+ const streamKey = [
1140
+ blockSessionId ? `session:${blockSessionId}` : '',
1141
+ explicitRequestId ? `request:${explicitRequestId}` : `content:${contentFingerprint}`,
1142
+ ].join('\u0000');
1143
+ const streamOccurrence = streamOccurrences.get(streamKey) ?? 0;
1144
+ streamOccurrences.set(streamKey, streamOccurrence + 1);
450
1145
  const cwd = blockCwd;
451
1146
  const skill = extractMarkdownLogSkill(`${userText}\n${assistantText}`);
452
1147
  const userContent = skill ? `<command-name>/${skill}</command-name>\n${userText}` : userText;
453
- const records = [{
454
- type: 'user',
455
- uuid: `markdown-log-${requestId}-user-${index}`,
456
- parentUuid: null,
457
- sessionId,
1148
+ const events = [];
1149
+ if (userContent) {
1150
+ events.push({
1151
+ eventKind: 'message',
1152
+ eventId: `markdown-log-${requestId}-user-${index}`,
1153
+ sourceIndex: 0,
1154
+ sourceType: 'markdown:user',
458
1155
  timestamp,
459
- message: { role: 'user', content: userContent },
460
- }, {
461
- type: 'assistant',
462
- uuid: `markdown-log-${requestId}-assistant-${index}`,
463
- parentUuid: `markdown-log-${requestId}-user-${index}`,
464
- sessionId,
1156
+ role: 'user',
1157
+ origin: 'human',
1158
+ text: userContent,
1159
+ });
1160
+ }
1161
+ if (assistantText) {
1162
+ events.push({
1163
+ eventKind: 'message',
1164
+ eventId: `markdown-log-${requestId}-assistant-${index}`,
1165
+ sourceIndex: 1,
1166
+ sourceType: 'markdown:assistant',
465
1167
  timestamp,
466
- cwd,
467
- attributionSkill: skill,
468
- message: {
469
- role: 'assistant',
470
- content: assistantText ? [{ type: 'text', text: assistantText }] : [],
471
- stop_reason: 'end_turn',
472
- },
473
- }];
1168
+ role: 'assistant',
1169
+ origin: 'synthetic',
1170
+ text: assistantText,
1171
+ attributionSkill: skill ?? undefined,
1172
+ });
1173
+ }
474
1174
  sessions.push({
475
- sessionId,
1175
+ runId: sessionId,
1176
+ rootRunId: sessionId,
1177
+ traceId: createTraceId({
1178
+ sourceKind: 'markdown_log',
1179
+ runId: sessionId,
1180
+ sourcePath: filePath,
1181
+ streamId: `${streamKey}\u0000occurrence:${streamOccurrence}`,
1182
+ }),
1183
+ groupPath: dirname(filePath),
1184
+ role: 'standalone',
1185
+ label: `${basename(filePath)}#${requestId}`,
476
1186
  sourcePath: filePath,
477
1187
  sourceKind: 'markdown_log',
478
- records,
1188
+ events,
479
1189
  cwd,
480
1190
  entrypoint: 'markdown_log',
481
1191
  startTimestamp: timestamp,
@@ -483,13 +1193,27 @@ function parseMarkdownLogFile(filePath) {
483
1193
  });
484
1194
  index += 1;
485
1195
  }
486
- return sessions;
1196
+ return {
1197
+ sessions,
1198
+ ingestion: {
1199
+ ...emptyTraceIngestionSummary(1),
1200
+ sourceRecordCount,
1201
+ parsedRecordCount: sessions.length,
1202
+ malformedRecordCount: Math.max(0, sourceRecordCount - sessions.length),
1203
+ },
1204
+ };
487
1205
  }
488
1206
  function markdownLogTimestampToIso(value) {
489
- const m = value.match(/^(\d{4})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/);
1207
+ const m = value.match(/^(\d{4})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s*(Z|[+-]\d{2}:?\d{2})$/);
490
1208
  if (!m)
491
- return new Date().toISOString();
492
- return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}+08:00`;
1209
+ return undefined;
1210
+ const rawOffset = m[7];
1211
+ if (!rawOffset)
1212
+ return undefined;
1213
+ const offset = rawOffset === 'Z' || rawOffset.includes(':')
1214
+ ? rawOffset
1215
+ : `${rawOffset.slice(0, 3)}:${rawOffset.slice(3)}`;
1216
+ return normalizeTraceTimestamp(`${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}${offset}`);
493
1217
  }
494
1218
  function extractMarkdownLogSection(body, startMarker, endMarker) {
495
1219
  const start = body.indexOf(startMarker);