oh-my-knowledge 0.28.0 → 0.29.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 +39 -0
- package/README.zh.md +39 -0
- package/dist/src/analysis/failure-clusterer.js +1 -1
- package/dist/src/analysis/failure-clusterer.js.map +1 -1
- package/dist/src/analysis/gap-analyzer.d.ts +8 -1
- package/dist/src/analysis/gap-analyzer.d.ts.map +1 -1
- package/dist/src/analysis/gap-analyzer.js +65 -33
- package/dist/src/analysis/gap-analyzer.js.map +1 -1
- package/dist/src/analysis/sample-diagnostics.js +14 -14
- package/dist/src/analysis/sample-diagnostics.js.map +1 -1
- package/dist/src/cli/commands/observe.d.ts.map +1 -1
- package/dist/src/cli/commands/observe.js +169 -0
- package/dist/src/cli/commands/observe.js.map +1 -1
- package/dist/src/cli/commands/registry.d.ts.map +1 -1
- package/dist/src/cli/commands/registry.js +9 -1
- package/dist/src/cli/commands/registry.js.map +1 -1
- package/dist/src/cli/commands/studio.d.ts.map +1 -1
- package/dist/src/cli/commands/studio.js +8 -0
- package/dist/src/cli/commands/studio.js.map +1 -1
- package/dist/src/cli/i18n-dict.d.ts +1 -1
- package/dist/src/cli/i18n-dict.d.ts.map +1 -1
- package/dist/src/cli/i18n-dict.js +125 -11
- package/dist/src/cli/i18n-dict.js.map +1 -1
- package/dist/src/grading/debias-validate.js +4 -4
- package/dist/src/grading/debias-validate.js.map +1 -1
- package/dist/src/grading/gold-cli.js +2 -2
- package/dist/src/grading/gold-cli.js.map +1 -1
- package/dist/src/observability/inbox-view-model.d.ts +17 -0
- package/dist/src/observability/inbox-view-model.d.ts.map +1 -0
- package/dist/src/observability/inbox-view-model.js +62 -0
- package/dist/src/observability/inbox-view-model.js.map +1 -0
- package/dist/src/observability/inbox.d.ts +100 -0
- package/dist/src/observability/inbox.d.ts.map +1 -0
- package/dist/src/observability/inbox.js +703 -0
- package/dist/src/observability/inbox.js.map +1 -0
- package/dist/src/observability/trace-adapter.d.ts +12 -64
- package/dist/src/observability/trace-adapter.d.ts.map +1 -1
- package/dist/src/observability/trace-adapter.js +9 -355
- package/dist/src/observability/trace-adapter.js.map +1 -1
- package/dist/src/observability/trace-attribution.d.ts +28 -0
- package/dist/src/observability/trace-attribution.d.ts.map +1 -0
- package/dist/src/observability/trace-attribution.js +104 -0
- package/dist/src/observability/trace-attribution.js.map +1 -0
- package/dist/src/observability/trace-segmenter.d.ts +42 -0
- package/dist/src/observability/trace-segmenter.d.ts.map +1 -0
- package/dist/src/observability/trace-segmenter.js +231 -0
- package/dist/src/observability/trace-segmenter.js.map +1 -0
- package/dist/src/observability/trace-source.d.ts +72 -0
- package/dist/src/observability/trace-source.d.ts.map +1 -0
- package/dist/src/observability/trace-source.js +142 -0
- package/dist/src/observability/trace-source.js.map +1 -0
- package/dist/src/renderer/html-renderer.d.ts.map +1 -1
- package/dist/src/renderer/html-renderer.js +107 -59
- package/dist/src/renderer/html-renderer.js.map +1 -1
- package/dist/src/renderer/layout.d.ts.map +1 -1
- package/dist/src/renderer/layout.js +58 -21
- package/dist/src/renderer/layout.js.map +1 -1
- package/dist/src/renderer/observation-inbox-renderer.d.ts +4 -0
- package/dist/src/renderer/observation-inbox-renderer.d.ts.map +1 -0
- package/dist/src/renderer/observation-inbox-renderer.js +900 -0
- package/dist/src/renderer/observation-inbox-renderer.js.map +1 -0
- package/dist/src/renderer/summary.d.ts +17 -0
- package/dist/src/renderer/summary.d.ts.map +1 -1
- package/dist/src/renderer/summary.js +277 -33
- package/dist/src/renderer/summary.js.map +1 -1
- package/dist/src/renderer/trends.d.ts.map +1 -1
- package/dist/src/renderer/trends.js +3 -1
- package/dist/src/renderer/trends.js.map +1 -1
- package/dist/src/server/report-server.d.ts +3 -1
- package/dist/src/server/report-server.d.ts.map +1 -1
- package/dist/src/server/report-server.js +35 -4
- package/dist/src/server/report-server.js.map +1 -1
- package/dist/src/shared/tool-search.d.ts +9 -0
- package/dist/src/shared/tool-search.d.ts.map +1 -0
- package/dist/src/shared/tool-search.js +104 -0
- package/dist/src/shared/tool-search.js.map +1 -0
- package/dist/src/types/executor.d.ts +4 -0
- package/dist/src/types/executor.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { extractGapSignalsFromTrace } from '../analysis/gap-analyzer.js';
|
|
6
|
+
import { ccTracesToResultEntries } from './trace-adapter.js';
|
|
7
|
+
import { isSearchToolCall, toolCallQuery } from '../shared/tool-search.js';
|
|
8
|
+
export const DEFAULT_PROJECT_OBSERVATIONS_DIR = join(process.cwd(), '.omk', 'observations');
|
|
9
|
+
export const DEFAULT_GLOBAL_OBSERVATIONS_DIR = join(homedir(), '.oh-my-knowledge', 'observations');
|
|
10
|
+
export const DEFAULT_OBSERVATIONS_DIR = DEFAULT_PROJECT_OBSERVATIONS_DIR;
|
|
11
|
+
function hashString(input) {
|
|
12
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
export function normalizeObservationKeyInput(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
const protocolMatch = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)(.*)$/i);
|
|
17
|
+
const prefix = protocolMatch?.[1] ?? '';
|
|
18
|
+
const body = protocolMatch?.[2] ?? trimmed;
|
|
19
|
+
return (prefix + body
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/^['"`]|['"`]$/g, '')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.replace(/:\d+(:\d+)?\b/g, '')
|
|
24
|
+
.replace(/\/+/g, '/')
|
|
25
|
+
.replace(/\/$/, ''));
|
|
26
|
+
}
|
|
27
|
+
export function inferObservationSourceKind(sourceTrace) {
|
|
28
|
+
if (sourceTrace.endsWith('.jsonl'))
|
|
29
|
+
return 'claude';
|
|
30
|
+
if (sourceTrace.endsWith('.log'))
|
|
31
|
+
return 'markdown_log';
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
function keyFor(item) {
|
|
35
|
+
const raw = item.signalSubtype === 'bash_probe'
|
|
36
|
+
? 'bash_probe'
|
|
37
|
+
: item.signalType === 'explicit_marker'
|
|
38
|
+
? item.evidence.markerToken || item.signalSubtype
|
|
39
|
+
: item.evidence.query || item.evidence.path || item.evidence.assistantSnippet || '';
|
|
40
|
+
return [
|
|
41
|
+
item.sourceKind,
|
|
42
|
+
item.skillName,
|
|
43
|
+
item.cwd ?? '',
|
|
44
|
+
item.signalType,
|
|
45
|
+
item.signalSubtype,
|
|
46
|
+
normalizeObservationKeyInput(raw),
|
|
47
|
+
].join('\u0000');
|
|
48
|
+
}
|
|
49
|
+
function markerTokenFromSignal(signal) {
|
|
50
|
+
const evidence = signal.evidence;
|
|
51
|
+
const marker = typeof evidence?.marker === 'string' ? evidence.marker : '';
|
|
52
|
+
const context = signal.context ?? '';
|
|
53
|
+
return marker || (context.match(/【推断】|【知识缺口】|【未知】|\[inferred\]|\[unknown\]|\[knowledge\s*gap\]/i)?.[0] ?? 'explicit_marker');
|
|
54
|
+
}
|
|
55
|
+
function snippet(value, max = 240) {
|
|
56
|
+
const text = typeof value === 'string' ? value : String(value ?? '');
|
|
57
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
58
|
+
return normalized ? normalized.slice(0, max) : undefined;
|
|
59
|
+
}
|
|
60
|
+
function isSuccessfulSearch(tc) {
|
|
61
|
+
if (!isSearchToolCall(tc) || tc.success === false)
|
|
62
|
+
return false;
|
|
63
|
+
const out = snippet(tc.output, 1000) ?? '';
|
|
64
|
+
return out !== '' && !/No matches found/i.test(out);
|
|
65
|
+
}
|
|
66
|
+
function bashCommand(tc) {
|
|
67
|
+
const input = (tc.input && typeof tc.input === 'object') ? tc.input : {};
|
|
68
|
+
return String(input.command ?? '');
|
|
69
|
+
}
|
|
70
|
+
function isBashProbe(tc) {
|
|
71
|
+
if (tc.tool !== 'Bash')
|
|
72
|
+
return false;
|
|
73
|
+
const command = bashCommand(tc);
|
|
74
|
+
if (/2>\s*\/dev\/null|\|\|\s*(true|echo\b)/.test(command))
|
|
75
|
+
return true;
|
|
76
|
+
return /\b(ls|test)\b.+(?:\/|\.\/|\.\.|~)/.test(command);
|
|
77
|
+
}
|
|
78
|
+
function failedSearchSubtype(tc, allCalls, index, skillName) {
|
|
79
|
+
const out = snippet(tc.output, 1000) ?? '';
|
|
80
|
+
if (isBashProbe(tc))
|
|
81
|
+
return 'bash_probe';
|
|
82
|
+
if (/permission denied|not authorized|eacces/i.test(out))
|
|
83
|
+
return 'permission_denied';
|
|
84
|
+
const q = toolCallQuery(tc);
|
|
85
|
+
const failedPath = q.path || q.query || '';
|
|
86
|
+
if (tc.tool === 'Read' && isSkillAssetPath(failedPath, skillName))
|
|
87
|
+
return 'skill_asset_read_failed';
|
|
88
|
+
if (/enoent|no such file or directory|path does not exist|file does not exist|not found|did you mean/i.test(out) && isTransientPath(failedPath))
|
|
89
|
+
return 'transient_file_missing';
|
|
90
|
+
if (/enoent|no such file or directory|path does not exist|file does not exist|not found|did you mean/i.test(out))
|
|
91
|
+
return 'not_found';
|
|
92
|
+
if (/exceeds maximum allowed tokens|maximum allowed tokens|token limit|timed out|timeout/i.test(out))
|
|
93
|
+
return 'tool_limit';
|
|
94
|
+
if (tc.success === false)
|
|
95
|
+
return 'tool_failure';
|
|
96
|
+
const laterSuccess = allCalls.slice(index + 1).some((later) => isSuccessfulSearch(later) && isSameTopicSearch(tc, later));
|
|
97
|
+
return laterSuccess ? 'exploratory_miss' : 'hard_miss';
|
|
98
|
+
}
|
|
99
|
+
function isTransientPath(value) {
|
|
100
|
+
return /(?:^|\s)(?:\/private)?\/tmp\/|(?:^|\s)\/var\/folders\/|figma_[\w.-]+\.(?:png|jpg|jpeg|webp)\b/i.test(value);
|
|
101
|
+
}
|
|
102
|
+
function isSkillAssetPath(value, skillName) {
|
|
103
|
+
if (!value || !skillName)
|
|
104
|
+
return false;
|
|
105
|
+
return value.includes(`/.claude/skills/${skillName}/`) || value.includes(`.claude/skills/${skillName}/`);
|
|
106
|
+
}
|
|
107
|
+
function topicTokens(value) {
|
|
108
|
+
return new Set(normalizeObservationKeyInput(value)
|
|
109
|
+
.split(/[^a-z0-9_\u4e00-\u9fff]+/i)
|
|
110
|
+
.map((part) => part.trim())
|
|
111
|
+
.filter((part) => part.length >= 3 && !['find', 'grep', 'head', 'tail', 'cat', 'src', 'path', 'file', 'repo'].includes(part)));
|
|
112
|
+
}
|
|
113
|
+
function isSameTopicSearch(a, b) {
|
|
114
|
+
const qa = toolCallQuery(a);
|
|
115
|
+
const qb = toolCallQuery(b);
|
|
116
|
+
if (qa.query) {
|
|
117
|
+
const queryTokens = topicTokens(qa.query);
|
|
118
|
+
if (queryTokens.size === 0)
|
|
119
|
+
return false;
|
|
120
|
+
// Bash 的 output 通常只是 path-echo (ls/cat/test 直接打印路径), 路径里的 cwd / repo
|
|
121
|
+
// token 会污染同主题判定。所以 Bash later success 只用 basename(structured path) 比对,
|
|
122
|
+
// 不读 output。Read 的 output 是文件内容, 用基名 + 内容做比对更可信。
|
|
123
|
+
const isBashLater = b.tool === 'Bash';
|
|
124
|
+
const laterText = qb.query
|
|
125
|
+
?? (isBashLater
|
|
126
|
+
? basenameForTopic(qb.path)
|
|
127
|
+
: `${basenameForTopic(qb.path)} ${snippet(b.output, 1000) ?? ''}`);
|
|
128
|
+
const laterTokens = topicTokens(laterText);
|
|
129
|
+
for (const token of queryTokens) {
|
|
130
|
+
if (laterTokens.has(token) || normalizeObservationKeyInput(laterText).includes(token))
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const left = qa.path ?? '';
|
|
136
|
+
const right = qb.query ?? qb.path ?? '';
|
|
137
|
+
const aTokens = topicTokens(left);
|
|
138
|
+
const bTokens = topicTokens(right);
|
|
139
|
+
for (const token of aTokens) {
|
|
140
|
+
if (bTokens.has(token))
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function basenameForTopic(value) {
|
|
146
|
+
if (!value)
|
|
147
|
+
return '';
|
|
148
|
+
return value.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
149
|
+
}
|
|
150
|
+
function confidenceForSubtype(subtype, signal) {
|
|
151
|
+
if (signal?.type === 'repeated_failure')
|
|
152
|
+
return 0.95;
|
|
153
|
+
if (subtype === 'repeated_failure')
|
|
154
|
+
return 0.95;
|
|
155
|
+
if (subtype === 'hard_miss')
|
|
156
|
+
return 0.9;
|
|
157
|
+
if (subtype === 'exploratory_miss' || subtype === 'bash_probe')
|
|
158
|
+
return 0.4;
|
|
159
|
+
if (subtype === 'skill_asset_read_failed')
|
|
160
|
+
return 0.4;
|
|
161
|
+
if (subtype === 'tool_error' || subtype === 'permission_error' || subtype === 'not_found' || subtype === 'transient_file_missing' || subtype === 'permission_denied' || subtype === 'tool_limit' || subtype === 'tool_failure')
|
|
162
|
+
return 0.2;
|
|
163
|
+
if (subtype === 'regex_only')
|
|
164
|
+
return 0.3;
|
|
165
|
+
if (subtype === 'llm_classified') {
|
|
166
|
+
const c = Number(signal?.classifierVerdict?.confidence);
|
|
167
|
+
return Number.isFinite(c) ? Math.max(0, Math.min(1, c)) : 0.5;
|
|
168
|
+
}
|
|
169
|
+
return 0.5;
|
|
170
|
+
}
|
|
171
|
+
function severityFor(signalType, subtype, confidence) {
|
|
172
|
+
if (signalType === 'repeated_failure' || signalType === 'explicit_marker')
|
|
173
|
+
return 'high';
|
|
174
|
+
if (subtype === 'hard_miss')
|
|
175
|
+
return 'high';
|
|
176
|
+
if (signalType === 'hedging' && confidence >= 0.7)
|
|
177
|
+
return 'high';
|
|
178
|
+
if (subtype === 'exploratory_miss' || subtype === 'bash_probe')
|
|
179
|
+
return 'medium';
|
|
180
|
+
if (subtype === 'skill_asset_read_failed')
|
|
181
|
+
return 'medium';
|
|
182
|
+
if (subtype === 'tool_error' || subtype === 'permission_error' || subtype === 'not_found' || subtype === 'transient_file_missing' || subtype === 'permission_denied' || subtype === 'tool_limit' || subtype === 'tool_failure' || subtype === 'regex_only')
|
|
183
|
+
return 'noise';
|
|
184
|
+
return 'low';
|
|
185
|
+
}
|
|
186
|
+
const SEVERITY_REASON_ZH = {
|
|
187
|
+
repeated_failure_suspected: '同类搜索连续失败 3 次以上,是强缺口信号,高于单次 hard_miss。',
|
|
188
|
+
explicit_gap_marker: 'agent 主动输出了知识缺口/未知标记,需要优先人工确认。',
|
|
189
|
+
knowledge_gap_suspected: '{tool}失败后,session 内未找到同主题成功证据,疑似知识缺口。',
|
|
190
|
+
exploratory_probe: 'skill 运行过程中出现了试路径、试目录或前序失败后后续成功的行为;先抽样确认,不直接判为要改 skill。',
|
|
191
|
+
skill_asset_unavailable: '读取该 skill 自身资源失败,可能是路径错位、资源未提交或 ignore 配置问题。',
|
|
192
|
+
soft_hedging_signal: '模型文本里出现了不确定表达,属于低置信文本信号,需要结合上下文人工判断。',
|
|
193
|
+
tool_or_runtime_noise: '更像路径、权限、文件太大、临时文件或工具运行问题;通常不作为 skill 内容缺失。',
|
|
194
|
+
};
|
|
195
|
+
const SEVERITY_REASON_EN = {
|
|
196
|
+
repeated_failure_suspected: 'Similar searches failed at least three times; this is stronger than a single hard miss.',
|
|
197
|
+
explicit_gap_marker: 'The agent explicitly marked an unknown or knowledge gap; review this first.',
|
|
198
|
+
knowledge_gap_suspected: '{tool}failed and no later same-topic success was found in the session.',
|
|
199
|
+
exploratory_probe: 'The event looks like path probing, directory probing, or an earlier miss followed by later success; sample it before changing the skill.',
|
|
200
|
+
skill_asset_unavailable: 'The agent failed to read an asset inside the skill itself; check path alignment, committed resources, or ignore rules.',
|
|
201
|
+
soft_hedging_signal: 'The agent used uncertain wording; treat this as a low-confidence text signal.',
|
|
202
|
+
tool_or_runtime_noise: 'This looks like a path, permission, token limit, transient file, or runtime tool issue rather than missing skill content.',
|
|
203
|
+
};
|
|
204
|
+
export function severityReasonFor(item, lang = 'zh') {
|
|
205
|
+
const code = item.severityReasonCode ?? severityReasonCodeFor(item);
|
|
206
|
+
const tool = item.evidence.tool ? `${item.evidence.tool} ` : '';
|
|
207
|
+
const dictionary = lang === 'en' ? SEVERITY_REASON_EN : SEVERITY_REASON_ZH;
|
|
208
|
+
return dictionary[code].replace('{tool}', tool);
|
|
209
|
+
}
|
|
210
|
+
export function legacySeverityReasonFor(item) {
|
|
211
|
+
const tool = item.evidence.tool ? `${item.evidence.tool} ` : '';
|
|
212
|
+
if (item.signalType === 'repeated_failure')
|
|
213
|
+
return '同类搜索连续失败 3 次以上,是强缺口信号,高于单次 hard_miss。';
|
|
214
|
+
if (item.signalType === 'explicit_marker')
|
|
215
|
+
return 'agent 主动输出了知识缺口/未知标记,需要优先人工确认。';
|
|
216
|
+
if (item.signalSubtype === 'hard_miss')
|
|
217
|
+
return `${tool}失败后,session 内未找到同主题成功证据,疑似知识缺口。`;
|
|
218
|
+
if (item.signalSubtype === 'bash_probe')
|
|
219
|
+
return 'skill 运行过程中,agent 调用了一条 Bash 命令;命令里用了 2>/dev/null 或 || true/echo 这类“失败也继续”的写法,更像是在试路径,不直接判为要改 skill。';
|
|
220
|
+
if (item.signalSubtype === 'exploratory_miss')
|
|
221
|
+
return '前序搜索失败,但后续出现成功搜索证据,更像探索性试错。';
|
|
222
|
+
if (item.signalSubtype === 'tool_limit')
|
|
223
|
+
return `${tool}触发 token/超时等工具限制,与知识库覆盖无直接关系。`;
|
|
224
|
+
if (item.signalSubtype === 'transient_file_missing')
|
|
225
|
+
return `${tool}访问的是 /tmp 或临时生成文件,默认按临时文件丢失处理,不作为 skill 内容缺失。`;
|
|
226
|
+
if (item.signalSubtype === 'skill_asset_read_failed')
|
|
227
|
+
return `${tool}读取该 skill 自身资源失败,可能是路径错位、资源未提交或 ignore 配置问题。`;
|
|
228
|
+
if (item.signalSubtype === 'not_found')
|
|
229
|
+
return `${tool}访问的文件或路径不存在,先按路径/环境噪声处理。`;
|
|
230
|
+
if (item.signalSubtype === 'permission_denied')
|
|
231
|
+
return `${tool}访问被权限拒绝,先按权限/环境噪声处理。`;
|
|
232
|
+
if (item.signalSubtype === 'tool_failure')
|
|
233
|
+
return `${tool}调用失败,先按运行时工具问题处理。`;
|
|
234
|
+
if (item.signalSubtype === 'regex_only')
|
|
235
|
+
return '仅命中 hedging 正则,假阳风险较高,默认作为低优先级噪声。';
|
|
236
|
+
if (item.signalSubtype === 'llm_classified')
|
|
237
|
+
return 'hedging 经过分类器确认,confidence 来自分类器输出。';
|
|
238
|
+
return '低置信信号,需要结合 evidence 和上下文人工判断。';
|
|
239
|
+
}
|
|
240
|
+
export function severityReasonCodeFor(item) {
|
|
241
|
+
if (item.signalSubtype === 'repeated_failure')
|
|
242
|
+
return 'repeated_failure_suspected';
|
|
243
|
+
if (item.signalType === 'explicit_marker')
|
|
244
|
+
return 'explicit_gap_marker';
|
|
245
|
+
if (item.signalSubtype === 'hard_miss')
|
|
246
|
+
return 'knowledge_gap_suspected';
|
|
247
|
+
if (item.signalSubtype === 'skill_asset_read_failed')
|
|
248
|
+
return 'skill_asset_unavailable';
|
|
249
|
+
if (item.signalSubtype === 'exploratory_miss' || item.signalSubtype === 'bash_probe')
|
|
250
|
+
return 'exploratory_probe';
|
|
251
|
+
if (item.signalType === 'hedging')
|
|
252
|
+
return 'soft_hedging_signal';
|
|
253
|
+
return 'tool_or_runtime_noise';
|
|
254
|
+
}
|
|
255
|
+
function itemsFromSegment(segment) {
|
|
256
|
+
// inbox 只观察真实 skill 调用场景。'general' 是 trace-adapter 对裸对话的
|
|
257
|
+
// 兜底归因(无 Skill tool / 无 <command-name> / 无 Read SKILL.md),
|
|
258
|
+
// attributionConfidence 仅 0.3,没有 skill 改进价值,直接过滤。
|
|
259
|
+
if (segment.skillName === 'general')
|
|
260
|
+
return [];
|
|
261
|
+
const sampleId = `${segment.sessionId}:${segment.segmentIndex}`;
|
|
262
|
+
const signals = extractGapSignalsFromTrace({
|
|
263
|
+
sampleId,
|
|
264
|
+
turns: segment.turns,
|
|
265
|
+
toolCalls: segment.toolCalls,
|
|
266
|
+
});
|
|
267
|
+
const items = [];
|
|
268
|
+
const toolCalls = segment.toolCalls;
|
|
269
|
+
const failedSignalsSeen = new Set();
|
|
270
|
+
for (const signal of signals) {
|
|
271
|
+
let subtype;
|
|
272
|
+
let evidence = {};
|
|
273
|
+
if (signal.type === 'failed_search') {
|
|
274
|
+
const matchIndex = toolCalls.findIndex((tc, i) => {
|
|
275
|
+
if (failedSignalsSeen.has(i))
|
|
276
|
+
return false;
|
|
277
|
+
const q = toolCallQuery(tc);
|
|
278
|
+
const signalPattern = String(signal.evidence?.pattern ?? '');
|
|
279
|
+
const sameQuery = !signalPattern
|
|
280
|
+
|| q.query === signalPattern
|
|
281
|
+
|| (tc.tool === 'Bash' && Boolean(q.query && (q.query.startsWith(signalPattern) || signalPattern.startsWith(q.query.slice(0, signalPattern.length)))));
|
|
282
|
+
return tc.tool === signal.evidence?.tool
|
|
283
|
+
&& sameQuery
|
|
284
|
+
&& (!signal.evidence?.path || q.path === signal.evidence.path);
|
|
285
|
+
});
|
|
286
|
+
const tc = matchIndex >= 0 ? toolCalls[matchIndex] : undefined;
|
|
287
|
+
if (matchIndex >= 0)
|
|
288
|
+
failedSignalsSeen.add(matchIndex);
|
|
289
|
+
subtype = tc ? failedSearchSubtype(tc, toolCalls, matchIndex, segment.skillName) : 'hard_miss';
|
|
290
|
+
const q = tc ? toolCallQuery(tc) : { query: String(signal.evidence?.pattern ?? ''), path: String(signal.evidence?.path ?? '') };
|
|
291
|
+
evidence = {
|
|
292
|
+
tool: snippet(signal.evidence?.tool ?? tc?.tool, 80),
|
|
293
|
+
query: snippet(q.query),
|
|
294
|
+
path: snippet(q.path),
|
|
295
|
+
outputSnippet: snippet(tc?.output),
|
|
296
|
+
messageIndex: tc?.messageIndex,
|
|
297
|
+
messageUuid: tc?.messageUuid,
|
|
298
|
+
toolUseId: tc?.toolUseId,
|
|
299
|
+
segmentTimestamp: tc?.timestamp ?? segment.startTimestamp,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
else if (signal.type === 'repeated_failure') {
|
|
303
|
+
subtype = 'repeated_failure';
|
|
304
|
+
evidence = { tool: snippet(signal.evidence?.tool, 80), outputSnippet: snippet(signal.context) };
|
|
305
|
+
}
|
|
306
|
+
else if (signal.type === 'hedging') {
|
|
307
|
+
subtype = signal.classifierVerdict ? 'llm_classified' : 'regex_only';
|
|
308
|
+
evidence = { assistantSnippet: snippet(signal.context) };
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
subtype = 'marker';
|
|
312
|
+
evidence = { markerToken: snippet(markerTokenFromSignal(signal), 80), assistantSnippet: snippet(signal.context) };
|
|
313
|
+
}
|
|
314
|
+
const signalType = signal.type;
|
|
315
|
+
const confidence = confidenceForSubtype(subtype, signal);
|
|
316
|
+
const severity = severityFor(signalType, subtype, confidence);
|
|
317
|
+
const item = {
|
|
318
|
+
id: hashString([segment.sessionId, segment.segmentIndex, signal.type, subtype, JSON.stringify(evidence)].join('\u0000')),
|
|
319
|
+
skillName: segment.skillName,
|
|
320
|
+
artifactVersion: 'unknown',
|
|
321
|
+
cwd: segment.cwd,
|
|
322
|
+
sessionId: segment.sessionId,
|
|
323
|
+
sourceTrace: '',
|
|
324
|
+
sourceKind: 'unknown',
|
|
325
|
+
signalType,
|
|
326
|
+
signalSubtype: subtype,
|
|
327
|
+
confidence,
|
|
328
|
+
attributionConfidence: segment.attribution?.confidence ?? 0.3,
|
|
329
|
+
severity,
|
|
330
|
+
severityReasonCode: severityReasonCodeFor({ signalType, signalSubtype: subtype, severity, confidence }),
|
|
331
|
+
evidence,
|
|
332
|
+
firstSeen: segment.startTimestamp,
|
|
333
|
+
lastSeen: segment.endTimestamp,
|
|
334
|
+
occurrences: 1,
|
|
335
|
+
recentSessionIds: [segment.sessionId],
|
|
336
|
+
representativeEvidence: [evidence],
|
|
337
|
+
};
|
|
338
|
+
items.push(item);
|
|
339
|
+
}
|
|
340
|
+
return items;
|
|
341
|
+
}
|
|
342
|
+
export function buildObservationInboxReport(tracePath) {
|
|
343
|
+
const { sessions, segments } = ccTracesToResultEntries(tracePath);
|
|
344
|
+
const sourceBySession = new Map(sessions.map((s) => [s.sessionId, s.sourcePath]));
|
|
345
|
+
const skillInvocationCounts = {};
|
|
346
|
+
const skillInvocationLastSeen = {};
|
|
347
|
+
const skillToolCallCounts = {};
|
|
348
|
+
const sessionsBySkill = new Map();
|
|
349
|
+
for (const segment of segments) {
|
|
350
|
+
if (segment.skillName === 'general')
|
|
351
|
+
continue;
|
|
352
|
+
skillInvocationCounts[segment.skillName] = (skillInvocationCounts[segment.skillName] ?? 0) + 1;
|
|
353
|
+
if (!skillInvocationLastSeen[segment.skillName] || segment.endTimestamp > skillInvocationLastSeen[segment.skillName]) {
|
|
354
|
+
skillInvocationLastSeen[segment.skillName] = segment.endTimestamp;
|
|
355
|
+
}
|
|
356
|
+
const toolCounts = skillToolCallCounts[segment.skillName] ?? {};
|
|
357
|
+
for (const toolCall of segment.toolCalls) {
|
|
358
|
+
toolCounts[toolCall.tool] = (toolCounts[toolCall.tool] ?? 0) + 1;
|
|
359
|
+
}
|
|
360
|
+
skillToolCallCounts[segment.skillName] = toolCounts;
|
|
361
|
+
const set = sessionsBySkill.get(segment.skillName) ?? new Set();
|
|
362
|
+
set.add(segment.sessionId);
|
|
363
|
+
sessionsBySkill.set(segment.skillName, set);
|
|
364
|
+
}
|
|
365
|
+
const skillSessionCounts = Object.fromEntries(Array.from(sessionsBySkill.entries()).map(([skill, sessionIds]) => [skill, sessionIds.size]));
|
|
366
|
+
const aggregationState = createInboxAggregationState();
|
|
367
|
+
for (const segment of segments) {
|
|
368
|
+
const sourceTrace = sourceBySession.get(segment.sessionId) ?? tracePath;
|
|
369
|
+
const segmentItems = itemsFromSegment(segment).map((item) => {
|
|
370
|
+
const withSource = {
|
|
371
|
+
...item,
|
|
372
|
+
sourceTrace,
|
|
373
|
+
sourceKind: inferObservationSourceKind(sourceTrace),
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
...withSource,
|
|
377
|
+
messageWindow: buildObservationMessageWindow(withSource),
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
addInboxItemsToState(aggregationState, segmentItems);
|
|
381
|
+
}
|
|
382
|
+
const items = finishInboxAggregation(aggregationState);
|
|
383
|
+
return {
|
|
384
|
+
kind: 'observe-inbox',
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
meta: {
|
|
387
|
+
tracePath,
|
|
388
|
+
generatedAt: new Date().toISOString(),
|
|
389
|
+
segmentCount: segments.length,
|
|
390
|
+
itemCount: items.length,
|
|
391
|
+
skillInvocationCounts,
|
|
392
|
+
skillSessionCounts,
|
|
393
|
+
skillInvocationLastSeen,
|
|
394
|
+
skillToolCallCounts,
|
|
395
|
+
},
|
|
396
|
+
items,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function createInboxAggregationState() {
|
|
400
|
+
return {
|
|
401
|
+
byKey: new Map(),
|
|
402
|
+
sessionLastSeenByKey: new Map(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function addInboxItemsToState(state, items) {
|
|
406
|
+
for (const item of items) {
|
|
407
|
+
const key = keyFor(item);
|
|
408
|
+
const sessionLastSeen = state.sessionLastSeenByKey.get(key) ?? new Map();
|
|
409
|
+
const previousSessionLastSeen = sessionLastSeen.get(item.sessionId);
|
|
410
|
+
if (!previousSessionLastSeen || item.lastSeen > previousSessionLastSeen) {
|
|
411
|
+
sessionLastSeen.set(item.sessionId, item.lastSeen);
|
|
412
|
+
}
|
|
413
|
+
state.sessionLastSeenByKey.set(key, sessionLastSeen);
|
|
414
|
+
const existing = state.byKey.get(key);
|
|
415
|
+
if (!existing) {
|
|
416
|
+
state.byKey.set(key, { ...item, representativeEvidence: [item.evidence], recentSessionIds: [item.sessionId] });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
existing.occurrences += item.occurrences;
|
|
420
|
+
if (item.firstSeen < existing.firstSeen)
|
|
421
|
+
existing.firstSeen = item.firstSeen;
|
|
422
|
+
if (item.lastSeen > existing.lastSeen)
|
|
423
|
+
existing.lastSeen = item.lastSeen;
|
|
424
|
+
existing.confidence = Math.max(existing.confidence, item.confidence);
|
|
425
|
+
existing.attributionConfidence = Math.max(existing.attributionConfidence, item.attributionConfidence);
|
|
426
|
+
if (severityRank(item.severity) > severityRank(existing.severity)
|
|
427
|
+
|| (item.severity === existing.severity && item.confidence > existing.confidence)) {
|
|
428
|
+
existing.severity = item.severity;
|
|
429
|
+
existing.severityReasonCode = item.severityReasonCode;
|
|
430
|
+
existing.evidence = item.evidence;
|
|
431
|
+
existing.messageWindow = item.messageWindow;
|
|
432
|
+
}
|
|
433
|
+
existing.recentSessionIds = Array.from(sessionLastSeen.entries())
|
|
434
|
+
.sort((a, b) => b[1].localeCompare(a[1]))
|
|
435
|
+
.map(([sessionId]) => sessionId)
|
|
436
|
+
.slice(0, 3);
|
|
437
|
+
existing.representativeEvidence.push(item.evidence);
|
|
438
|
+
existing.representativeEvidence = existing.representativeEvidence.slice(0, 50);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function finishInboxAggregation(state) {
|
|
442
|
+
return Array.from(state.byKey.values()).sort(compareInboxItems);
|
|
443
|
+
}
|
|
444
|
+
export function aggregateInboxItems(items) {
|
|
445
|
+
const state = createInboxAggregationState();
|
|
446
|
+
addInboxItemsToState(state, items);
|
|
447
|
+
return finishInboxAggregation(state);
|
|
448
|
+
}
|
|
449
|
+
function severityRank(severity) {
|
|
450
|
+
if (severity === 'high')
|
|
451
|
+
return 4;
|
|
452
|
+
if (severity === 'medium')
|
|
453
|
+
return 3;
|
|
454
|
+
if (severity === 'low')
|
|
455
|
+
return 2;
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
function compareInboxItems(a, b) {
|
|
459
|
+
const severity = severityRank(b.severity) - severityRank(a.severity);
|
|
460
|
+
if (severity !== 0)
|
|
461
|
+
return severity;
|
|
462
|
+
const confidence = b.confidence - a.confidence;
|
|
463
|
+
if (confidence !== 0)
|
|
464
|
+
return confidence;
|
|
465
|
+
const lastSeen = b.lastSeen.localeCompare(a.lastSeen);
|
|
466
|
+
if (lastSeen !== 0)
|
|
467
|
+
return lastSeen;
|
|
468
|
+
return b.occurrences - a.occurrences;
|
|
469
|
+
}
|
|
470
|
+
export function saveObservationInboxReport(report, outDir = DEFAULT_OBSERVATIONS_DIR) {
|
|
471
|
+
mkdirSync(outDir, { recursive: true });
|
|
472
|
+
// 保留毫秒;同秒不同毫秒生成的两份 report 不应静默互相覆盖。
|
|
473
|
+
// 例: '2026-05-07T12:00:00.999Z' → '2026-05-07T12-00-00-999'
|
|
474
|
+
const stamp = report.meta.generatedAt.replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
475
|
+
const path = join(outDir, `${stamp}-observe-inbox.json`);
|
|
476
|
+
writeFileSync(path, JSON.stringify(report, null, 2));
|
|
477
|
+
return path;
|
|
478
|
+
}
|
|
479
|
+
export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
480
|
+
if (!existsSync(dir)) {
|
|
481
|
+
if (dir === DEFAULT_PROJECT_OBSERVATIONS_DIR && existsSync(DEFAULT_GLOBAL_OBSERVATIONS_DIR)) {
|
|
482
|
+
return loadObservationInboxReports(DEFAULT_GLOBAL_OBSERVATIONS_DIR);
|
|
483
|
+
}
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
return readdirSync(dir)
|
|
487
|
+
.filter((file) => file.endsWith('-observe-inbox.json'))
|
|
488
|
+
.map((file) => {
|
|
489
|
+
try {
|
|
490
|
+
const report = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
491
|
+
report.items = report.items.map((item) => {
|
|
492
|
+
const sourceKind = item.sourceKind;
|
|
493
|
+
return {
|
|
494
|
+
...item,
|
|
495
|
+
sourceKind: sourceKind === 'openclaw'
|
|
496
|
+
? 'markdown_log'
|
|
497
|
+
: (item.sourceKind ?? inferObservationSourceKind(item.sourceTrace)),
|
|
498
|
+
severityReasonCode: item.severityReasonCode ?? severityReasonCodeFor(item),
|
|
499
|
+
severityReason: undefined,
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
return report;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
.filter((r) => r?.kind === 'observe-inbox');
|
|
509
|
+
}
|
|
510
|
+
export function loadLatestObservationInboxReport(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
511
|
+
const reports = loadObservationInboxReports(dir);
|
|
512
|
+
if (reports.length === 0)
|
|
513
|
+
return null;
|
|
514
|
+
return reports.sort((a, b) => b.meta.generatedAt.localeCompare(a.meta.generatedAt))[0] ?? null;
|
|
515
|
+
}
|
|
516
|
+
export function loadLatestObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
517
|
+
const latest = loadLatestObservationInboxReport(dir);
|
|
518
|
+
return latest ? [latest] : [];
|
|
519
|
+
}
|
|
520
|
+
export function queryObservationInbox(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
521
|
+
const reports = loadLatestObservationInboxReports(dir);
|
|
522
|
+
return aggregateInboxItems(reports.flatMap((report) => report.items));
|
|
523
|
+
}
|
|
524
|
+
export function summarizeObservationInboxBySkill(items, reports = []) {
|
|
525
|
+
const invocationCounts = reports.reduce((acc, report) => {
|
|
526
|
+
for (const [skill, count] of Object.entries(report.meta.skillInvocationCounts ?? {})) {
|
|
527
|
+
acc[skill] = (acc[skill] ?? 0) + count;
|
|
528
|
+
}
|
|
529
|
+
return acc;
|
|
530
|
+
}, {});
|
|
531
|
+
const sessionCounts = reports.reduce((acc, report) => {
|
|
532
|
+
for (const [skill, count] of Object.entries(report.meta.skillSessionCounts ?? {})) {
|
|
533
|
+
acc[skill] = (acc[skill] ?? 0) + count;
|
|
534
|
+
}
|
|
535
|
+
return acc;
|
|
536
|
+
}, {});
|
|
537
|
+
const bySkill = new Map();
|
|
538
|
+
for (const item of items) {
|
|
539
|
+
const group = bySkill.get(item.skillName) ?? [];
|
|
540
|
+
group.push(item);
|
|
541
|
+
bySkill.set(item.skillName, group);
|
|
542
|
+
}
|
|
543
|
+
const skillNames = new Set([...Object.keys(invocationCounts), ...bySkill.keys()]);
|
|
544
|
+
return Array.from(skillNames).map((skillName) => {
|
|
545
|
+
const group = bySkill.get(skillName) ?? [];
|
|
546
|
+
return {
|
|
547
|
+
skillName,
|
|
548
|
+
invocationCount: invocationCounts[skillName] ?? group.reduce((sum, item) => sum + item.occurrences, 0),
|
|
549
|
+
sessionCount: sessionCounts[skillName] ?? new Set(group.flatMap((item) => item.recentSessionIds)).size,
|
|
550
|
+
observationCount: group.length,
|
|
551
|
+
highCount: group.filter((item) => item.severity === 'high').length,
|
|
552
|
+
mediumCount: group.filter((item) => item.severity === 'medium').length,
|
|
553
|
+
lowCount: group.filter((item) => item.severity === 'low').length,
|
|
554
|
+
noiseCount: group.filter((item) => item.severity === 'noise').length,
|
|
555
|
+
latestSeen: group.reduce((latest, item) => item.lastSeen > latest ? item.lastSeen : latest, ''),
|
|
556
|
+
};
|
|
557
|
+
}).sort((a, b) => {
|
|
558
|
+
const riskA = a.highCount * 100 + a.mediumCount * 10 + a.lowCount;
|
|
559
|
+
const riskB = b.highCount * 100 + b.mediumCount * 10 + b.lowCount;
|
|
560
|
+
if (riskB !== riskA)
|
|
561
|
+
return riskB - riskA;
|
|
562
|
+
return b.invocationCount - a.invocationCount;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
export function findObservationInboxItem(id, dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
566
|
+
const reports = loadObservationInboxReports(dir);
|
|
567
|
+
for (const item of [...reports].reverse().flatMap((report) => report.items)) {
|
|
568
|
+
if (item.id === id)
|
|
569
|
+
return item;
|
|
570
|
+
}
|
|
571
|
+
return aggregateInboxItems(reports.flatMap((report) => report.items)).find((item) => item.id === id) ?? null;
|
|
572
|
+
}
|
|
573
|
+
export function buildObservationMessageWindow(item, radius = 3) {
|
|
574
|
+
if (!item.sourceTrace.endsWith('.jsonl'))
|
|
575
|
+
return undefined;
|
|
576
|
+
const index = item.evidence.messageIndex;
|
|
577
|
+
if (typeof index !== 'number' || index < 0 || !existsSync(item.sourceTrace))
|
|
578
|
+
return undefined;
|
|
579
|
+
const messages = readJsonlMessageRefs(item.sourceTrace);
|
|
580
|
+
if (messages.length === 0)
|
|
581
|
+
return undefined;
|
|
582
|
+
const position = messages.findIndex((message) => message.messageIndex === index);
|
|
583
|
+
if (position < 0)
|
|
584
|
+
return undefined;
|
|
585
|
+
const before = messages.slice(Math.max(0, position - radius), position);
|
|
586
|
+
const event = messages.slice(position, position + 1);
|
|
587
|
+
let after = messages.slice(position + 1, Math.min(messages.length, position + radius + 1));
|
|
588
|
+
if (item.evidence.toolUseId) {
|
|
589
|
+
const result = messages.find((message) => message.messageIndex > index && message.snippet.includes(item.evidence.toolUseId));
|
|
590
|
+
if (result && !event.some((message) => message.messageIndex === result.messageIndex)) {
|
|
591
|
+
event.push(result);
|
|
592
|
+
after = after.filter((message) => message.messageIndex !== result.messageIndex);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
before,
|
|
597
|
+
event,
|
|
598
|
+
after,
|
|
599
|
+
resolutionAfter: inferResolutionAfter(messages.slice(index + 1), item.signalType),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
export function formatObservationShow(item) {
|
|
603
|
+
const window = item.messageWindow ?? buildObservationMessageWindow(item);
|
|
604
|
+
const lines = [
|
|
605
|
+
`Observation ${item.id}`,
|
|
606
|
+
`skill=${item.skillName} severity=${item.severity} signal=${item.signalType}/${item.signalSubtype} confidence=${item.confidence.toFixed(2)} attribution=${item.attributionConfidence.toFixed(2)}`,
|
|
607
|
+
`source=${item.sourceTrace}`,
|
|
608
|
+
item.severityReasonCode ? `reason=${item.severityReasonCode}` : '',
|
|
609
|
+
'',
|
|
610
|
+
].filter(Boolean);
|
|
611
|
+
if (!window) {
|
|
612
|
+
lines.push('No message window available for this observation.');
|
|
613
|
+
return lines.join('\n');
|
|
614
|
+
}
|
|
615
|
+
lines.push('--- 上文 ---');
|
|
616
|
+
lines.push(...formatMessageRefs(window.before));
|
|
617
|
+
lines.push('--- 失败点 / 触发点 ---');
|
|
618
|
+
lines.push(...formatMessageRefs(window.event));
|
|
619
|
+
lines.push('--- 下文 ---');
|
|
620
|
+
lines.push(...formatMessageRefs(window.after));
|
|
621
|
+
lines.push(`resolutionAfter=${window.resolutionAfter}`);
|
|
622
|
+
return lines.join('\n');
|
|
623
|
+
}
|
|
624
|
+
function readJsonlMessageRefs(path) {
|
|
625
|
+
return readFileSync(path, 'utf-8')
|
|
626
|
+
.split('\n')
|
|
627
|
+
.map((line, index) => {
|
|
628
|
+
const trimmed = line.trim();
|
|
629
|
+
if (!trimmed)
|
|
630
|
+
return null;
|
|
631
|
+
try {
|
|
632
|
+
const record = JSON.parse(trimmed);
|
|
633
|
+
const type = String(record.type ?? 'other');
|
|
634
|
+
if (type !== 'user' && type !== 'assistant')
|
|
635
|
+
return null;
|
|
636
|
+
const message = record.message;
|
|
637
|
+
const role = message?.role === 'assistant' ? 'assistant' : message?.role === 'user' ? 'user' : type === 'assistant' ? 'assistant' : type === 'user' ? 'user' : 'other';
|
|
638
|
+
return {
|
|
639
|
+
role,
|
|
640
|
+
snippet: snippet(extractRecordText(record), 500) ?? '',
|
|
641
|
+
messageIndex: index,
|
|
642
|
+
uuid: typeof record.uuid === 'string' ? record.uuid : undefined,
|
|
643
|
+
timestamp: typeof record.timestamp === 'string' ? record.timestamp : undefined,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
.filter((message) => message !== null && message.snippet !== '');
|
|
651
|
+
}
|
|
652
|
+
function extractRecordText(record) {
|
|
653
|
+
const message = record.message;
|
|
654
|
+
const content = message?.content;
|
|
655
|
+
if (typeof content === 'string')
|
|
656
|
+
return content;
|
|
657
|
+
if (Array.isArray(content)) {
|
|
658
|
+
return content.map((part) => {
|
|
659
|
+
if (!part || typeof part !== 'object')
|
|
660
|
+
return '';
|
|
661
|
+
const obj = part;
|
|
662
|
+
if (obj.type === 'text')
|
|
663
|
+
return String(obj.text ?? '');
|
|
664
|
+
if (obj.type === 'tool_use')
|
|
665
|
+
return `tool_use ${String(obj.name ?? '')} ${String(obj.id ?? '')} ${JSON.stringify(obj.input ?? {})}`;
|
|
666
|
+
if (obj.type === 'tool_result')
|
|
667
|
+
return `tool_result ${String(obj.tool_use_id ?? '')} ${String(obj.content ?? '')}`;
|
|
668
|
+
if (obj.type === 'thinking')
|
|
669
|
+
return '';
|
|
670
|
+
return JSON.stringify(obj);
|
|
671
|
+
}).filter(Boolean).join('\n');
|
|
672
|
+
}
|
|
673
|
+
return JSON.stringify(record);
|
|
674
|
+
}
|
|
675
|
+
function inferResolutionAfter(messages, signalType) {
|
|
676
|
+
if (signalType !== 'failed_search' && signalType !== 'repeated_failure')
|
|
677
|
+
return 'unknown';
|
|
678
|
+
const later = messages.map((message) => message.snippet).join('\n');
|
|
679
|
+
if (/No matches found|does not exist|not found|permission denied|exceeds maximum allowed tokens/i.test(later) && !/tool_result [^\s]+ (?!No matches found)/i.test(later)) {
|
|
680
|
+
return 'unresolved';
|
|
681
|
+
}
|
|
682
|
+
if (/tool_result [^\s]+ (?!No matches found).{10,}/i.test(later))
|
|
683
|
+
return 'resolved';
|
|
684
|
+
return 'unknown';
|
|
685
|
+
}
|
|
686
|
+
function formatMessageRefs(messages) {
|
|
687
|
+
if (messages.length === 0)
|
|
688
|
+
return ['(none)'];
|
|
689
|
+
return messages.map((message) => `[${message.messageIndex}] ${message.role}${message.timestamp ? ` ${message.timestamp}` : ''}${message.uuid ? ` ${message.uuid}` : ''}\n${message.snippet}`);
|
|
690
|
+
}
|
|
691
|
+
export function selectExploreInboxItems(items, limit, includeNoise = false, random = Math.random) {
|
|
692
|
+
const candidates = items
|
|
693
|
+
.filter((item) => item.severity === 'medium' || item.severity === 'low' || (includeNoise && item.severity === 'noise'))
|
|
694
|
+
.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen))
|
|
695
|
+
.slice(0, 50);
|
|
696
|
+
const shuffled = [...candidates];
|
|
697
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
698
|
+
const j = Math.floor(random() * (i + 1));
|
|
699
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
700
|
+
}
|
|
701
|
+
return shuffled.slice(0, limit);
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=inbox.js.map
|