@bububuger/spanory 0.1.15 → 0.1.18
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/dist/alert/evaluate.js +151 -0
- package/dist/env.d.ts +4 -0
- package/dist/env.js +40 -12
- package/dist/index.js +364 -38
- package/dist/issue/state.d.ts +39 -0
- package/dist/issue/state.js +151 -0
- package/dist/otlp.d.ts +2 -2
- package/dist/report/aggregate.d.ts +1 -0
- package/dist/report/aggregate.js +128 -2
- package/dist/runtime/claude/adapter.js +151 -2
- package/dist/runtime/codex/adapter.js +39 -3
- package/dist/runtime/codex/proxy.js +4 -1
- package/dist/runtime/openclaw/adapter.js +140 -2
- package/dist/runtime/shared/file-settle.d.ts +19 -0
- package/dist/runtime/shared/file-settle.js +44 -0
- package/dist/runtime/shared/normalize.js +464 -18
- package/package.json +8 -6
package/dist/alert/evaluate.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { summarizeAgents, summarizeCache, summarizeCommands, summarizeMcp, summarizeSessions, summarizeTurnDiff, } from '../report/aggregate.js';
|
|
4
|
+
const DEFAULT_CONTEXT_WINDOW_TOKENS = 200000;
|
|
4
5
|
function getMetricFromSessionRow(row, metric, refs = {}) {
|
|
5
6
|
const cacheRow = refs.cacheBySessionId?.get(row.sessionId);
|
|
6
7
|
const agentRow = refs.agentBySessionId?.get(row.sessionId);
|
|
7
8
|
const turnDiffRows = refs.turnDiffBySessionId?.get(row.sessionId) ?? [];
|
|
9
|
+
const contextRow = refs.contextBySessionId?.get(row.sessionId);
|
|
8
10
|
switch (metric) {
|
|
9
11
|
case 'events':
|
|
10
12
|
return row.events ?? 0;
|
|
@@ -26,10 +28,154 @@ function getMetricFromSessionRow(row, metric, refs = {}) {
|
|
|
26
28
|
return agentRow?.agentTasks ?? 0;
|
|
27
29
|
case 'diff.char_delta.max':
|
|
28
30
|
return turnDiffRows.reduce((max, rowItem) => Math.max(max, Math.abs(Number(rowItem.charDelta ?? 0))), 0);
|
|
31
|
+
case 'context.unknown_delta_share.window5':
|
|
32
|
+
return contextRow?.unknownDeltaShareWindow5 ?? 0;
|
|
33
|
+
case 'context.unknown_top_streak':
|
|
34
|
+
return contextRow?.unknownTopStreak ?? 0;
|
|
35
|
+
case 'context.high_pollution_source_streak':
|
|
36
|
+
return contextRow?.highPollutionSourceStreak ?? 0;
|
|
37
|
+
case 'context.fill_ratio.max':
|
|
38
|
+
return contextRow?.maxFillRatio ?? 0;
|
|
39
|
+
case 'context.delta_ratio.max':
|
|
40
|
+
return contextRow?.maxDeltaRatio ?? 0;
|
|
41
|
+
case 'context.compact.count':
|
|
42
|
+
return contextRow?.compactCount ?? 0;
|
|
29
43
|
default:
|
|
30
44
|
return 0;
|
|
31
45
|
}
|
|
32
46
|
}
|
|
47
|
+
function parseJsonObject(value) {
|
|
48
|
+
if (typeof value !== 'string' || !value.trim())
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(value);
|
|
52
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore parse errors
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function parseJsonArray(value) {
|
|
61
|
+
if (typeof value !== 'string' || !value.trim())
|
|
62
|
+
return [];
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(value);
|
|
65
|
+
if (Array.isArray(parsed))
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore parse errors
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
function summarizeContextForSession(events) {
|
|
74
|
+
const snapshots = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_snapshot');
|
|
75
|
+
const attributions = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_source_attribution');
|
|
76
|
+
const last5 = snapshots.slice(-5);
|
|
77
|
+
let unknownTokens = 0;
|
|
78
|
+
let totalTokens = 0;
|
|
79
|
+
for (const snapshot of last5) {
|
|
80
|
+
const composition = parseJsonObject(snapshot?.attributes?.['agentic.context.composition']);
|
|
81
|
+
if (!composition)
|
|
82
|
+
continue;
|
|
83
|
+
for (const [kind, raw] of Object.entries(composition)) {
|
|
84
|
+
const value = Number(raw);
|
|
85
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
86
|
+
continue;
|
|
87
|
+
totalTokens += value;
|
|
88
|
+
if (kind === 'unknown')
|
|
89
|
+
unknownTokens += value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const unknownDeltaShareWindow5 = totalTokens > 0 ? unknownTokens / totalTokens : 0;
|
|
93
|
+
let unknownTopStreak = 0;
|
|
94
|
+
let runningUnknown = 0;
|
|
95
|
+
for (const snapshot of snapshots) {
|
|
96
|
+
const topSources = parseJsonArray(snapshot?.attributes?.['agentic.context.top_sources']);
|
|
97
|
+
const top = String(topSources[0] ?? '').trim();
|
|
98
|
+
if (top === 'unknown') {
|
|
99
|
+
runningUnknown += 1;
|
|
100
|
+
if (runningUnknown > unknownTopStreak)
|
|
101
|
+
unknownTopStreak = runningUnknown;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
runningUnknown = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const turnOrder = [];
|
|
108
|
+
const highByTurn = new Map();
|
|
109
|
+
for (const event of attributions) {
|
|
110
|
+
const attrs = event?.attributes ?? {};
|
|
111
|
+
const turnId = String(event?.turnId ?? '');
|
|
112
|
+
if (!turnId)
|
|
113
|
+
continue;
|
|
114
|
+
if (!highByTurn.has(turnId)) {
|
|
115
|
+
highByTurn.set(turnId, []);
|
|
116
|
+
turnOrder.push(turnId);
|
|
117
|
+
}
|
|
118
|
+
const sourceKind = String(attrs['agentic.context.source_kind'] ?? '').trim();
|
|
119
|
+
const score = Number(attrs['agentic.context.pollution_score']);
|
|
120
|
+
if (!sourceKind || !Number.isFinite(score))
|
|
121
|
+
continue;
|
|
122
|
+
if (score < 80)
|
|
123
|
+
continue;
|
|
124
|
+
highByTurn.get(turnId).push({ sourceKind, score });
|
|
125
|
+
}
|
|
126
|
+
let highPollutionSourceStreak = 0;
|
|
127
|
+
let runningSource = '';
|
|
128
|
+
let runningCount = 0;
|
|
129
|
+
for (const turnId of turnOrder) {
|
|
130
|
+
const items = highByTurn.get(turnId) ?? [];
|
|
131
|
+
if (!items.length) {
|
|
132
|
+
runningSource = '';
|
|
133
|
+
runningCount = 0;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
items.sort((a, b) => b.score - a.score);
|
|
137
|
+
const topSource = items[0].sourceKind;
|
|
138
|
+
if (topSource === runningSource) {
|
|
139
|
+
runningCount += 1;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
runningSource = topSource;
|
|
143
|
+
runningCount = 1;
|
|
144
|
+
}
|
|
145
|
+
if (runningCount > highPollutionSourceStreak) {
|
|
146
|
+
highPollutionSourceStreak = runningCount;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
let maxFillRatio = 0;
|
|
150
|
+
let maxDeltaRatio = 0;
|
|
151
|
+
let compactCount = 0;
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
const attrs = event?.attributes ?? {};
|
|
154
|
+
if (String(attrs['agentic.context.event_type'] ?? '') === 'context_snapshot') {
|
|
155
|
+
const fillRatio = Number(attrs['agentic.context.fill_ratio']);
|
|
156
|
+
if (Number.isFinite(fillRatio) && fillRatio > maxFillRatio)
|
|
157
|
+
maxFillRatio = fillRatio;
|
|
158
|
+
const deltaTokens = Number(attrs['agentic.context.delta_tokens']);
|
|
159
|
+
if (Number.isFinite(deltaTokens) && deltaTokens > 0) {
|
|
160
|
+
const ratio = deltaTokens / DEFAULT_CONTEXT_WINDOW_TOKENS;
|
|
161
|
+
if (ratio > maxDeltaRatio)
|
|
162
|
+
maxDeltaRatio = ratio;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (String(attrs['agentic.context.event_type'] ?? '') === 'context_boundary'
|
|
166
|
+
&& String(attrs['agentic.context.boundary_kind'] ?? '') === 'compact_after') {
|
|
167
|
+
compactCount += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
unknownDeltaShareWindow5,
|
|
172
|
+
unknownTopStreak,
|
|
173
|
+
highPollutionSourceStreak,
|
|
174
|
+
maxFillRatio,
|
|
175
|
+
maxDeltaRatio,
|
|
176
|
+
compactCount,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
33
179
|
function getMetricFromAgentRow(row, metric) {
|
|
34
180
|
switch (metric) {
|
|
35
181
|
case 'agentTasks':
|
|
@@ -68,12 +214,17 @@ function evaluateSessionRule(rule, sessions) {
|
|
|
68
214
|
list.push(row);
|
|
69
215
|
turnDiffBySessionId.set(key, list);
|
|
70
216
|
}
|
|
217
|
+
const contextBySessionId = new Map(sessions.map((session) => [
|
|
218
|
+
session.context?.sessionId ?? session.events?.[0]?.sessionId,
|
|
219
|
+
summarizeContextForSession(session.events ?? []),
|
|
220
|
+
]));
|
|
71
221
|
const matched = rows
|
|
72
222
|
.map((row) => {
|
|
73
223
|
const value = getMetricFromSessionRow(row, rule.metric, {
|
|
74
224
|
cacheBySessionId,
|
|
75
225
|
agentBySessionId,
|
|
76
226
|
turnDiffBySessionId,
|
|
227
|
+
contextBySessionId,
|
|
77
228
|
});
|
|
78
229
|
return { row, value };
|
|
79
230
|
})
|
package/dist/env.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
export declare function resolveUserHome(): string;
|
|
2
|
+
export declare function resolveSpanoryHome(): string;
|
|
3
|
+
export declare function resolveSpanoryEnvPath(): string;
|
|
4
|
+
export declare function resolveLegacyUserEnvPath(): string;
|
|
1
5
|
export declare function parseSimpleDotEnv(raw: any): {};
|
|
2
6
|
export declare function loadUserEnv(): Promise<void>;
|
package/dist/env.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
function resolveUserHome() {
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
export function resolveUserHome() {
|
|
5
5
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
6
6
|
}
|
|
7
|
+
export function resolveSpanoryHome() {
|
|
8
|
+
if (process.env.SPANORY_HOME)
|
|
9
|
+
return process.env.SPANORY_HOME;
|
|
10
|
+
const home = resolveUserHome();
|
|
11
|
+
return home ? path.join(home, '.spanory') : '';
|
|
12
|
+
}
|
|
13
|
+
export function resolveSpanoryEnvPath() {
|
|
14
|
+
const root = resolveSpanoryHome();
|
|
15
|
+
return root ? path.join(root, '.env') : '';
|
|
16
|
+
}
|
|
17
|
+
export function resolveLegacyUserEnvPath() {
|
|
18
|
+
const home = resolveUserHome();
|
|
19
|
+
return home ? path.join(home, '.env') : '';
|
|
20
|
+
}
|
|
7
21
|
export function parseSimpleDotEnv(raw) {
|
|
8
22
|
const out = {};
|
|
9
23
|
for (const line of String(raw).split('\n')) {
|
|
@@ -36,19 +50,33 @@ export function parseSimpleDotEnv(raw) {
|
|
|
36
50
|
return out;
|
|
37
51
|
}
|
|
38
52
|
export async function loadUserEnv() {
|
|
39
|
-
const
|
|
40
|
-
|
|
53
|
+
const spanoryHome = resolveSpanoryHome();
|
|
54
|
+
const envPath = resolveSpanoryEnvPath();
|
|
55
|
+
const legacyEnvPath = resolveLegacyUserEnvPath();
|
|
56
|
+
if (!spanoryHome || !envPath)
|
|
41
57
|
return;
|
|
42
|
-
const envPath = path.join(home, '.env');
|
|
43
58
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
47
|
-
if (process.env[k] === undefined)
|
|
48
|
-
process.env[k] = v;
|
|
49
|
-
}
|
|
59
|
+
await mkdir(spanoryHome, { recursive: true });
|
|
60
|
+
await writeFile(envPath, '', { flag: 'a' });
|
|
50
61
|
}
|
|
51
62
|
catch {
|
|
52
|
-
//
|
|
63
|
+
// best effort only
|
|
64
|
+
}
|
|
65
|
+
const candidates = [envPath, legacyEnvPath].filter(Boolean);
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = await readFile(candidate, 'utf-8');
|
|
69
|
+
const parsed = parseSimpleDotEnv(raw);
|
|
70
|
+
if (Object.keys(parsed).length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
73
|
+
if (process.env[k] === undefined)
|
|
74
|
+
process.env[k] = v;
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// try next candidate
|
|
80
|
+
}
|
|
53
81
|
}
|
|
54
82
|
}
|