@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { RUNTIME_CAPABILITIES } from '../shared/capabilities.js';
|
|
5
5
|
import { normalizeTranscriptMessages, parseProjectIdFromTranscriptPath, pickUsage } from '../shared/normalize.js';
|
|
6
|
+
const INFER_WINDOW_EPSILON_MS = 1200;
|
|
6
7
|
function parseTimestamp(entry) {
|
|
7
8
|
const raw = entry?.timestamp ?? entry?.created_at ?? entry?.createdAt;
|
|
8
9
|
const date = raw ? new Date(raw) : new Date();
|
|
@@ -140,6 +141,139 @@ function normalizeAgentId(entry) {
|
|
|
140
141
|
?? entry?.payload?.agentId
|
|
141
142
|
?? entry?.payload?.agent_id);
|
|
142
143
|
}
|
|
144
|
+
function extractToolUses(content) {
|
|
145
|
+
if (!Array.isArray(content))
|
|
146
|
+
return [];
|
|
147
|
+
return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_use');
|
|
148
|
+
}
|
|
149
|
+
function extractToolResults(content) {
|
|
150
|
+
if (!Array.isArray(content))
|
|
151
|
+
return [];
|
|
152
|
+
return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_result');
|
|
153
|
+
}
|
|
154
|
+
function isToolResultOnlyContent(content) {
|
|
155
|
+
return Array.isArray(content)
|
|
156
|
+
&& content.length > 0
|
|
157
|
+
&& content.every((block) => block && typeof block === 'object' && block.type === 'tool_result');
|
|
158
|
+
}
|
|
159
|
+
function isPromptUserMessage(message) {
|
|
160
|
+
if (!message || message.role !== 'user' || message.isMeta)
|
|
161
|
+
return false;
|
|
162
|
+
const { content } = message;
|
|
163
|
+
if (typeof content === 'string')
|
|
164
|
+
return content.trim().length > 0;
|
|
165
|
+
if (!Array.isArray(content))
|
|
166
|
+
return false;
|
|
167
|
+
if (isToolResultOnlyContent(content))
|
|
168
|
+
return false;
|
|
169
|
+
return content.length > 0;
|
|
170
|
+
}
|
|
171
|
+
function findChildSessionHints(messages) {
|
|
172
|
+
const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || String(m?.agentId ?? '').trim().length > 0);
|
|
173
|
+
if (!hasSidechainSignal)
|
|
174
|
+
return null;
|
|
175
|
+
const hasParentLink = messages.some((m) => String(m?.parentSessionId ?? m?.parent_session_id ?? '').trim().length > 0
|
|
176
|
+
|| String(m?.parentTurnId ?? m?.parent_turn_id ?? '').trim().length > 0
|
|
177
|
+
|| String(m?.parentToolCallId ?? m?.parent_tool_call_id ?? '').trim().length > 0);
|
|
178
|
+
if (hasParentLink)
|
|
179
|
+
return null;
|
|
180
|
+
const sorted = [...messages].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
181
|
+
const childStartedAt = sorted[0]?.timestamp;
|
|
182
|
+
if (!childStartedAt)
|
|
183
|
+
return null;
|
|
184
|
+
return { childStartedAt };
|
|
185
|
+
}
|
|
186
|
+
function extractTaskWindows(messages, sessionId) {
|
|
187
|
+
const sorted = [...messages].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
188
|
+
const windows = [];
|
|
189
|
+
const byCallId = new Map();
|
|
190
|
+
let turnIndex = 0;
|
|
191
|
+
let currentTurnId = 'turn-1';
|
|
192
|
+
for (const msg of sorted) {
|
|
193
|
+
if (isPromptUserMessage(msg)) {
|
|
194
|
+
turnIndex += 1;
|
|
195
|
+
currentTurnId = `turn-${turnIndex}`;
|
|
196
|
+
}
|
|
197
|
+
if (msg.role === 'assistant') {
|
|
198
|
+
for (const tu of extractToolUses(msg.content)) {
|
|
199
|
+
const toolName = String(tu.name ?? '').trim();
|
|
200
|
+
if (toolName !== 'Task')
|
|
201
|
+
continue;
|
|
202
|
+
const callId = String(tu.id ?? '').trim();
|
|
203
|
+
if (!callId)
|
|
204
|
+
continue;
|
|
205
|
+
const window = {
|
|
206
|
+
parentSessionId: sessionId,
|
|
207
|
+
parentTurnId: currentTurnId,
|
|
208
|
+
parentToolCallId: callId,
|
|
209
|
+
startedAtMs: msg.timestamp.getTime(),
|
|
210
|
+
endedAtMs: msg.timestamp.getTime(),
|
|
211
|
+
};
|
|
212
|
+
windows.push(window);
|
|
213
|
+
byCallId.set(callId, window);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (msg.role === 'user') {
|
|
217
|
+
for (const tr of extractToolResults(msg.content)) {
|
|
218
|
+
const callId = String(tr.tool_use_id ?? tr.toolUseId ?? '').trim();
|
|
219
|
+
if (!callId)
|
|
220
|
+
continue;
|
|
221
|
+
const window = byCallId.get(callId);
|
|
222
|
+
if (window) {
|
|
223
|
+
window.endedAtMs = Math.max(window.endedAtMs, msg.timestamp.getTime());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return windows;
|
|
229
|
+
}
|
|
230
|
+
async function inferParentLinkFromSiblingSessions({ transcriptPath, messages }) {
|
|
231
|
+
const hints = findChildSessionHints(messages);
|
|
232
|
+
if (!hints)
|
|
233
|
+
return messages;
|
|
234
|
+
const currentSessionId = path.basename(transcriptPath, '.jsonl');
|
|
235
|
+
const dir = path.dirname(transcriptPath);
|
|
236
|
+
let entries = [];
|
|
237
|
+
try {
|
|
238
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return messages;
|
|
242
|
+
}
|
|
243
|
+
const candidates = [];
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
246
|
+
continue;
|
|
247
|
+
const siblingSessionId = entry.name.slice(0, -'.jsonl'.length);
|
|
248
|
+
if (siblingSessionId === currentSessionId)
|
|
249
|
+
continue;
|
|
250
|
+
const siblingPath = path.join(dir, entry.name);
|
|
251
|
+
const siblingMessages = await readOpenclawTranscript(siblingPath);
|
|
252
|
+
const windows = extractTaskWindows(siblingMessages, siblingSessionId);
|
|
253
|
+
for (const window of windows) {
|
|
254
|
+
const childAtMs = hints.childStartedAt.getTime();
|
|
255
|
+
const lower = window.startedAtMs - INFER_WINDOW_EPSILON_MS;
|
|
256
|
+
const upper = window.endedAtMs + INFER_WINDOW_EPSILON_MS;
|
|
257
|
+
if (childAtMs < lower || childAtMs > upper)
|
|
258
|
+
continue;
|
|
259
|
+
candidates.push({
|
|
260
|
+
...window,
|
|
261
|
+
score: Math.abs(childAtMs - window.startedAtMs),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (candidates.length === 0)
|
|
266
|
+
return messages;
|
|
267
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
268
|
+
const best = candidates[0];
|
|
269
|
+
return messages.map((msg) => ({
|
|
270
|
+
...msg,
|
|
271
|
+
parentSessionId: best.parentSessionId,
|
|
272
|
+
parentTurnId: best.parentTurnId,
|
|
273
|
+
parentToolCallId: best.parentToolCallId,
|
|
274
|
+
parentLinkConfidence: 'inferred',
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
143
277
|
async function readOpenclawTranscript(transcriptPath) {
|
|
144
278
|
const raw = await readFile(transcriptPath, 'utf-8');
|
|
145
279
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
@@ -165,6 +299,9 @@ async function readOpenclawTranscript(transcriptPath) {
|
|
|
165
299
|
isMeta: entry?.isMeta ?? entry?.is_meta ?? false,
|
|
166
300
|
isSidechain: normalizeIsSidechain(entry),
|
|
167
301
|
agentId: normalizeAgentId(entry),
|
|
302
|
+
parentSessionId: entry?.parentSessionId ?? entry?.parent_session_id,
|
|
303
|
+
parentTurnId: entry?.parentTurnId ?? entry?.parent_turn_id,
|
|
304
|
+
parentToolCallId: entry?.parentToolCallId ?? entry?.parent_tool_call_id,
|
|
168
305
|
content: normalizeContent(entry),
|
|
169
306
|
model: normalizeModel(entry),
|
|
170
307
|
usage: normalizeUsage(entry),
|
|
@@ -231,7 +368,8 @@ export const openclawAdapter = {
|
|
|
231
368
|
},
|
|
232
369
|
async collectEvents(context) {
|
|
233
370
|
const transcriptPath = await resolveTranscriptPath(context);
|
|
234
|
-
const
|
|
371
|
+
const loaded = await readOpenclawTranscript(transcriptPath);
|
|
372
|
+
const messages = await inferParentLinkFromSiblingSessions({ transcriptPath, messages: loaded });
|
|
235
373
|
return normalizeTranscriptMessages({
|
|
236
374
|
runtime: 'openclaw',
|
|
237
375
|
projectId: context.projectId,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type FileStat = {
|
|
2
|
+
mtimeMs: number;
|
|
3
|
+
};
|
|
4
|
+
type WaitForFileSettleOptions = {
|
|
5
|
+
filePath: string;
|
|
6
|
+
stableWindowMs?: number;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
pollMs?: number;
|
|
9
|
+
statFn?: (filePath: string) => Promise<FileStat>;
|
|
10
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
11
|
+
nowFn?: () => number;
|
|
12
|
+
};
|
|
13
|
+
type WaitForFileSettleResult = {
|
|
14
|
+
settled: boolean;
|
|
15
|
+
lastMtimeMs?: number;
|
|
16
|
+
waitedMs: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function waitForFileMtimeToSettle(options: WaitForFileSettleOptions): Promise<WaitForFileSettleResult>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
function sleep(ms) {
|
|
3
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
}
|
|
5
|
+
export async function waitForFileMtimeToSettle(options) {
|
|
6
|
+
const stableWindowMs = options.stableWindowMs ?? 400;
|
|
7
|
+
const timeoutMs = options.timeoutMs ?? 2500;
|
|
8
|
+
const pollMs = options.pollMs ?? 100;
|
|
9
|
+
const statFn = options.statFn ?? (async (filePath) => stat(filePath));
|
|
10
|
+
const sleepFn = options.sleepFn ?? sleep;
|
|
11
|
+
const nowFn = options.nowFn ?? Date.now;
|
|
12
|
+
const startedAt = nowFn();
|
|
13
|
+
const deadline = startedAt + timeoutMs;
|
|
14
|
+
let lastMtimeMs;
|
|
15
|
+
let unchangedSinceMs;
|
|
16
|
+
while (nowFn() <= deadline) {
|
|
17
|
+
const current = await statFn(options.filePath);
|
|
18
|
+
const mtimeMs = Number(current.mtimeMs);
|
|
19
|
+
if (lastMtimeMs === mtimeMs) {
|
|
20
|
+
if (unchangedSinceMs === undefined)
|
|
21
|
+
unchangedSinceMs = nowFn();
|
|
22
|
+
if (nowFn() - unchangedSinceMs >= stableWindowMs) {
|
|
23
|
+
return {
|
|
24
|
+
settled: true,
|
|
25
|
+
lastMtimeMs: mtimeMs,
|
|
26
|
+
waitedMs: nowFn() - startedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
lastMtimeMs = mtimeMs;
|
|
32
|
+
unchangedSinceMs = undefined;
|
|
33
|
+
}
|
|
34
|
+
const remainingMs = deadline - nowFn();
|
|
35
|
+
if (remainingMs <= 0)
|
|
36
|
+
break;
|
|
37
|
+
await sleepFn(Math.min(pollMs, remainingMs));
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
settled: false,
|
|
41
|
+
lastMtimeMs,
|
|
42
|
+
waitedMs: nowFn() - startedAt,
|
|
43
|
+
};
|
|
44
|
+
}
|