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.
- package/README.md +50 -18
- package/README.zh.md +55 -23
- package/dist/analysis/coverage-analyzer.d.ts +1 -0
- package/dist/analysis/coverage-analyzer.js +125 -62
- package/dist/analysis/failure-clusterer.js +2 -1
- package/dist/analysis/gap-analyzer.d.ts +2 -2
- package/dist/analysis/gap-analyzer.js +13 -3
- package/dist/analysis/hedging-classifier.d.ts +2 -2
- package/dist/analysis/hedging-classifier.js +3 -4
- package/dist/analysis/report-diagnostics.js +9 -7
- package/dist/analysis/sample-diagnostics.js +6 -6
- package/dist/artifact-graph/doctor.js +15 -7
- package/dist/assets/agent-skills/omk/SKILL.md +27 -7
- package/dist/assets/agent-skills/omk/references/commands.md +19 -17
- package/dist/authoring/evolver.d.ts +10 -6
- package/dist/authoring/evolver.js +496 -83
- package/dist/authoring/generator.d.ts +3 -3
- package/dist/authoring/generator.js +5 -10
- package/dist/authoring/sample-fixer.d.ts +8 -6
- package/dist/authoring/sample-fixer.js +76 -5
- package/dist/cli/commands/doctor.js +31 -14
- package/dist/cli/commands/eval/index.d.ts +3 -0
- package/dist/cli/commands/eval/index.js +163 -17
- package/dist/cli/commands/evolve.d.ts +4 -4
- package/dist/cli/commands/evolve.js +27 -13
- package/dist/cli/commands/init.js +16 -3
- package/dist/cli/commands/observe/inbox.js +44 -21
- package/dist/cli/commands/observe/index.js +20 -11
- package/dist/cli/commands/observe/ingest.d.ts +3 -0
- package/dist/cli/commands/observe/ingest.js +35 -4
- package/dist/cli/commands/sample.d.ts +9 -3
- package/dist/cli/commands/sample.js +91 -74
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/codex-model-hint.d.ts +9 -0
- package/dist/cli/lib/codex-model-hint.js +45 -0
- package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
- package/dist/cli/lib/generation-failure-hint.js +61 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +4 -0
- package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/gen.js +38 -6
- package/dist/cli/lib/i18n-dict/help.js +6 -6
- package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/init.js +13 -9
- package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/run.js +34 -2
- package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
- package/dist/cli/lib/llm-failure-classifier.js +8 -0
- package/dist/cli/lib/parse-run-config.d.ts +6 -5
- package/dist/cli/lib/parse-run-config.js +16 -9
- package/dist/cli/lib/runtime-defaults.d.ts +21 -0
- package/dist/cli/lib/runtime-defaults.js +79 -0
- package/dist/diagnosis/observe-mapper.js +14 -15
- package/dist/diagnosis/observe-producer.js +3 -1
- package/dist/diagnosis/studio-projection.js +14 -7
- package/dist/diagnosis/types.d.ts +2 -0
- package/dist/diagnosis/types.js +12 -0
- package/dist/doctor/endpoint-rule.js +2 -1
- package/dist/eval-core/artifact-file-names.js +18 -1
- package/dist/eval-core/artifact-index.d.ts +7 -11
- package/dist/eval-core/artifact-index.js +139 -80
- package/dist/eval-core/cache.d.ts +12 -3
- package/dist/eval-core/cache.js +89 -29
- package/dist/eval-core/comparability.js +10 -6
- package/dist/eval-core/evaluation-execution.d.ts +2 -1
- package/dist/eval-core/evaluation-execution.js +122 -37
- package/dist/eval-core/evaluation-job.d.ts +4 -1
- package/dist/eval-core/evaluation-job.js +4 -1
- package/dist/eval-core/evaluation-reporting.d.ts +15 -13
- package/dist/eval-core/evaluation-reporting.js +54 -52
- package/dist/eval-core/execution-strategy.d.ts +2 -0
- package/dist/eval-core/execution-strategy.js +11 -9
- package/dist/eval-core/fact-checker.js +15 -7
- package/dist/eval-core/holdout.js +3 -2
- package/dist/eval-core/judge-independence.d.ts +2 -2
- package/dist/eval-core/mock-hook.cjs +23 -6
- package/dist/eval-core/mocks-runtime.js +30 -8
- package/dist/eval-core/report-document.d.ts +12 -0
- package/dist/eval-core/report-document.js +1151 -0
- package/dist/eval-core/report-extensions.d.ts +4 -0
- package/dist/eval-core/report-extensions.js +500 -0
- package/dist/eval-core/report-file-migration.js +7 -2
- package/dist/eval-core/resume-compatibility.d.ts +31 -0
- package/dist/eval-core/resume-compatibility.js +141 -0
- package/dist/eval-core/sample-fingerprint.d.ts +12 -0
- package/dist/eval-core/sample-fingerprint.js +193 -0
- package/dist/eval-core/schema.js +86 -31
- package/dist/eval-core/verdict.d.ts +8 -4
- package/dist/eval-core/verdict.js +24 -10
- package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
- package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
- package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
- package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
- package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
- package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
- package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
- package/dist/eval-workflows/evaluation-pipeline.js +12 -7
- package/dist/eval-workflows/run-evaluation.d.ts +9 -7
- package/dist/eval-workflows/run-evaluation.js +79 -51
- package/dist/executors/anthropic-api.js +65 -9
- package/dist/executors/claude-cli.js +16 -79
- package/dist/executors/claude-protocol.d.ts +28 -0
- package/dist/executors/claude-protocol.js +180 -0
- package/dist/executors/claude-sdk-trace.js +56 -28
- package/dist/executors/claude-sdk.d.ts +1 -0
- package/dist/executors/claude-sdk.js +39 -93
- package/dist/executors/codex-cli-trace.js +166 -31
- package/dist/executors/codex-cli.d.ts +6 -8
- package/dist/executors/codex-cli.js +49 -151
- package/dist/executors/codex-protocol.d.ts +24 -0
- package/dist/executors/codex-protocol.js +234 -0
- package/dist/executors/codex-sdk.js +68 -120
- package/dist/executors/gemini.js +88 -13
- package/dist/executors/index.d.ts +2 -3
- package/dist/executors/index.js +5 -3
- package/dist/executors/openai-api.js +70 -9
- package/dist/executors/runtime-fingerprint.js +88 -11
- package/dist/executors/script-command.d.ts +8 -0
- package/dist/executors/script-command.js +87 -0
- package/dist/executors/script.js +202 -29
- package/dist/executors/shared.d.ts +35 -3
- package/dist/executors/shared.js +113 -15
- package/dist/grading/assertions.d.ts +1 -1
- package/dist/grading/assertions.js +19 -9
- package/dist/grading/diagnostic.d.ts +9 -2
- package/dist/grading/diagnostic.js +25 -2
- package/dist/grading/index.js +10 -4
- package/dist/grading/judge.js +19 -6
- package/dist/grading/layered-scores.d.ts +2 -3
- package/dist/grading/layered-scores.js +2 -3
- package/dist/inputs/load-samples.d.ts +1 -2
- package/dist/inputs/load-samples.js +23 -1
- package/dist/inputs/mcp-resolver.js +6 -3
- package/dist/inputs/sample-document.d.ts +11 -0
- package/dist/inputs/sample-document.js +96 -0
- package/dist/managed/evidence.d.ts +1 -0
- package/dist/managed/evidence.js +1 -1
- package/dist/managed/store.js +200 -91
- package/dist/observability/codex-trace-adapter.d.ts +5 -0
- package/dist/observability/codex-trace-adapter.js +850 -0
- package/dist/observability/experience.d.ts +32 -6
- package/dist/observability/experience.js +2695 -459
- package/dist/observability/feedback-matchers.js +16 -1
- package/dist/observability/inbox-view-model.d.ts +2 -1
- package/dist/observability/inbox-view-model.js +20 -14
- package/dist/observability/inbox.d.ts +7 -1
- package/dist/observability/inbox.js +632 -124
- package/dist/observability/problem-patterns.js +2 -0
- package/dist/observability/review-state.d.ts +6 -0
- package/dist/observability/review-state.js +235 -63
- package/dist/observability/skill-chain-advisories.js +1 -1
- package/dist/observability/skill-chain.js +17 -4
- package/dist/observability/skill-health-analyzer.d.ts +32 -7
- package/dist/observability/skill-health-analyzer.js +194 -121
- package/dist/observability/skill-health-report.d.ts +10 -0
- package/dist/observability/skill-health-report.js +620 -0
- package/dist/observability/soft-standards/constants.d.ts +0 -1
- package/dist/observability/soft-standards/constants.js +0 -1
- package/dist/observability/soft-standards/index.d.ts +1 -1
- package/dist/observability/soft-standards/index.js +1 -1
- package/dist/observability/soft-standards/llm-extractor.js +8 -10
- package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
- package/dist/observability/soft-standards/skill-standards-store.js +59 -18
- package/dist/observability/soft-standards/types.d.ts +2 -2
- package/dist/observability/trace-adapter.d.ts +12 -7
- package/dist/observability/trace-adapter.js +11 -9
- package/dist/observability/trace-attribution.d.ts +13 -5
- package/dist/observability/trace-attribution.js +315 -21
- package/dist/observability/trace-ingestion.d.ts +9 -0
- package/dist/observability/trace-ingestion.js +80 -0
- package/dist/observability/trace-ir.d.ts +113 -0
- package/dist/observability/trace-ir.js +87 -0
- package/dist/observability/trace-segmenter.d.ts +19 -6
- package/dist/observability/trace-segmenter.js +377 -196
- package/dist/observability/trace-session-index.d.ts +19 -0
- package/dist/observability/trace-session-index.js +68 -0
- package/dist/observability/trace-source.d.ts +12 -4
- package/dist/observability/trace-source.js +939 -215
- package/dist/renderer/html-renderer.js +37 -6
- package/dist/renderer/icons.js +3 -0
- package/dist/renderer/observation-inbox-renderer.js +227 -91
- package/dist/renderer/skill-detail-renderer.js +452 -109
- package/dist/renderer/skill-health-renderer.js +69 -12
- package/dist/renderer/summary.js +28 -7
- package/dist/renderer/table.js +21 -4
- package/dist/renderer/test-view.d.ts +1 -0
- package/dist/renderer/test-view.js +44 -9
- package/dist/server/indexed-report-store.js +14 -18
- package/dist/server/job-store.js +64 -26
- package/dist/server/report-server.js +190 -78
- package/dist/server/report-store.js +57 -80
- package/dist/server/skill-index.js +143 -49
- package/dist/server/skill-insights.js +44 -5
- package/dist/shared/artifact-graph.d.ts +3 -0
- package/dist/shared/artifact-graph.js +224 -0
- package/dist/shared/assertion-types.d.ts +8 -0
- package/dist/shared/assertion-types.js +46 -0
- package/dist/shared/atomic-json.d.ts +8 -0
- package/dist/shared/atomic-json.js +33 -0
- package/dist/shared/diagnosis-schema.d.ts +9 -0
- package/dist/shared/diagnosis-schema.js +181 -0
- package/dist/shared/doctor-report.d.ts +3 -0
- package/dist/shared/doctor-report.js +103 -0
- package/dist/shared/evaluation-job.d.ts +6 -0
- package/dist/shared/evaluation-job.js +217 -0
- package/dist/shared/executor-result.d.ts +17 -0
- package/dist/shared/executor-result.js +221 -0
- package/dist/shared/file-lock.d.ts +12 -0
- package/dist/shared/file-lock.js +129 -0
- package/dist/shared/json-value.d.ts +5 -0
- package/dist/shared/json-value.js +36 -0
- package/dist/shared/keyed-mutex.d.ts +7 -0
- package/dist/shared/keyed-mutex.js +24 -0
- package/dist/shared/record-count.d.ts +8 -0
- package/dist/shared/record-count.js +43 -0
- package/dist/shared/sample-contract.d.ts +3 -0
- package/dist/shared/sample-contract.js +332 -0
- package/dist/shared/shell-quote.d.ts +2 -0
- package/dist/shared/shell-quote.js +7 -0
- package/dist/shared/timestamp.d.ts +6 -0
- package/dist/shared/timestamp.js +64 -0
- package/dist/shared/token-usage.d.ts +19 -0
- package/dist/shared/token-usage.js +50 -0
- package/dist/shared/tool-call-status.d.ts +8 -0
- package/dist/shared/tool-call-status.js +28 -0
- package/dist/shared/tool-identity.d.ts +21 -0
- package/dist/shared/tool-identity.js +84 -0
- package/dist/shared/tool-search.js +73 -16
- package/dist/shared/trace-projection.d.ts +5 -0
- package/dist/shared/trace-projection.js +20 -0
- package/dist/shared/trace-source-kind.d.ts +3 -0
- package/dist/shared/trace-source-kind.js +12 -0
- package/dist/types/diagnosis.d.ts +2 -0
- package/dist/types/eval.d.ts +4 -0
- package/dist/types/executor.d.ts +32 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/judge.d.ts +2 -0
- package/dist/types/observability.d.ts +116 -9
- package/dist/types/report.d.ts +58 -6
- package/dist/types/skill-index.d.ts +7 -0
- package/dist/types/trace.d.ts +2 -0
- package/dist/types/trace.js +1 -0
- package/package.json +9 -5
|
@@ -1,25 +1,68 @@
|
|
|
1
|
-
/** Trace source loading and parsing for Claude
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
traceId: traceIdFor(session),
|
|
55
|
-
|
|
56
|
-
|
|
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?.
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
traceId: traceIdFor(session),
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
const first = records.find((r) => 'sessionId' in r && typeof r.sessionId === 'string');
|
|
135
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
455
|
+
events,
|
|
141
456
|
cwd: first?.cwd,
|
|
142
457
|
gitBranch: first?.gitBranch,
|
|
143
458
|
entrypoint: first?.entrypoint,
|
|
144
|
-
|
|
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
|
|
759
|
+
const sessionRecord = rawRecords.find((record) => record?.type === 'session');
|
|
154
760
|
const sessionId = typeof sessionRecord?.id === 'string'
|
|
155
761
|
? sessionRecord.id
|
|
156
|
-
: filePath
|
|
762
|
+
: basename(filePath, '.jsonl');
|
|
157
763
|
const cwd = typeof sessionRecord?.cwd === 'string' ? sessionRecord.cwd : undefined;
|
|
158
764
|
const sourceMetadata = extractOpenClawSourceMetadata(rawRecords);
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
783
|
+
events,
|
|
176
784
|
cwd,
|
|
177
785
|
entrypoint: 'openclaw',
|
|
178
786
|
sourceMetadata,
|
|
179
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
},
|
|
280
|
-
};
|
|
921
|
+
role: 'user',
|
|
922
|
+
origin: classifyUserMessageOrigin({ entrypoint: 'openclaw' }, text),
|
|
923
|
+
text,
|
|
924
|
+
}));
|
|
281
925
|
}
|
|
282
926
|
if (message.role === 'assistant') {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
|
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
|
|
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
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
492
|
-
|
|
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);
|