openclaw-observability 2026.3.21 → 2026.3.23
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/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +805 -98
- package/dist/index.js.map +1 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +8 -1
- package/dist/redaction.js.map +1 -1
- package/dist/security/scanner.d.ts +15 -1
- package/dist/security/scanner.d.ts.map +1 -1
- package/dist/security/scanner.js +148 -11
- package/dist/security/scanner.js.map +1 -1
- package/dist/security/types.d.ts +1 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/security/types.js +1 -0
- package/dist/security/types.js.map +1 -1
- package/dist/storage/duckdb-local-writer.d.ts +2 -1
- package/dist/storage/duckdb-local-writer.d.ts.map +1 -1
- package/dist/storage/duckdb-local-writer.js +20 -7
- package/dist/storage/duckdb-local-writer.js.map +1 -1
- package/dist/storage/mysql-writer.d.ts +3 -1
- package/dist/storage/mysql-writer.d.ts.map +1 -1
- package/dist/storage/mysql-writer.js +11 -6
- package/dist/storage/mysql-writer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/web/api.d.ts +2 -2
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +121 -57
- package/dist/web/api.js.map +1 -1
- package/dist/web/routes.d.ts +7 -3
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +176 -25
- package/dist/web/routes.js.map +1 -1
- package/dist/web/ui.js +1190 -141
- package/dist/web/ui.js.map +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,13 +6,46 @@
|
|
|
6
6
|
* OpenClaw hooks:
|
|
7
7
|
* Agent: before_model_resolve, before_prompt_build, before_agent_start, agent_end
|
|
8
8
|
* LLM: llm_input, llm_output
|
|
9
|
-
* Tool: before_tool_call, after_tool_call
|
|
9
|
+
* Tool: before_tool_call, after_tool_call
|
|
10
10
|
* Msg: message_received, message_sending, message_sent, before_message_write
|
|
11
11
|
* Ctx: before_compaction, after_compaction, before_reset
|
|
12
12
|
* Session: session_start, session_end
|
|
13
13
|
* Subagent: subagent_spawning, subagent_delivery_target, subagent_spawned, subagent_ended
|
|
14
14
|
* Gateway: gateway_start, gateway_stop
|
|
15
15
|
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
16
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
50
|
exports.activate = activate;
|
|
18
51
|
const config_1 = require("./config");
|
|
@@ -23,6 +56,10 @@ const duckdb_local_writer_1 = require("./storage/duckdb-local-writer");
|
|
|
23
56
|
const buffer_1 = require("./storage/buffer");
|
|
24
57
|
const routes_1 = require("./web/routes");
|
|
25
58
|
const scanner_1 = require("./security/scanner");
|
|
59
|
+
const fs = __importStar(require("node:fs"));
|
|
60
|
+
const path = __importStar(require("node:path"));
|
|
61
|
+
const os = __importStar(require("node:os"));
|
|
62
|
+
const node_crypto_1 = require("node:crypto");
|
|
26
63
|
/* ------------------------------------------------------------------ */
|
|
27
64
|
/* Runtime state */
|
|
28
65
|
/* ------------------------------------------------------------------ */
|
|
@@ -32,8 +69,16 @@ const MAX_MAP_ENTRIES = 1000;
|
|
|
32
69
|
const MAP_ENTRY_TTL_MS = 300_000;
|
|
33
70
|
/** Max alert buffer size */
|
|
34
71
|
const MAX_ALERT_BUFFER = 500;
|
|
35
|
-
/** Track start time of each LLM
|
|
36
|
-
const
|
|
72
|
+
/** Track start time of each LLM call (runId -> timestamp) */
|
|
73
|
+
const llmRunStartTimes = new Map();
|
|
74
|
+
/** Resolve runtime event stream records back to observability sessions */
|
|
75
|
+
const runSessionIds = new Map();
|
|
76
|
+
/** Track start times of tool calls (supports same-key concurrency via FIFO queue) */
|
|
77
|
+
const toolCallStartTimes = new Map();
|
|
78
|
+
const pendingRuntimeEvents = new Map();
|
|
79
|
+
const runtimeTextSeen = new Set();
|
|
80
|
+
const replayedAssistantMessages = new Map();
|
|
81
|
+
const sessionTranscriptPathCache = new Map();
|
|
37
82
|
/**
|
|
38
83
|
* Map: requestUrl → usage from SSE final chunk
|
|
39
84
|
* We key by a monotonically increasing request ID since we can't know
|
|
@@ -41,15 +86,242 @@ const runStartTimes = new Map();
|
|
|
41
86
|
*/
|
|
42
87
|
const sseUsageCache = new Map();
|
|
43
88
|
let sseRequestCounter = 0;
|
|
44
|
-
/**
|
|
45
|
-
* Most recent SSE usage entry (LIFO — the llm_output hook fires shortly
|
|
46
|
-
* after the stream completes, so the latest entry is the one we want)
|
|
47
|
-
*/
|
|
48
|
-
let latestSseUsage = null;
|
|
49
89
|
/** Original fetch reference */
|
|
50
90
|
let originalFetch = null;
|
|
51
91
|
/** Whether the fetch interceptor is installed */
|
|
52
92
|
let fetchInterceptorInstalled = false;
|
|
93
|
+
function getRuntimeTextSeenKey(runId, stream) {
|
|
94
|
+
return `${runId}:${stream}`;
|
|
95
|
+
}
|
|
96
|
+
function parseMessageTimestamp(value) {
|
|
97
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
98
|
+
return value > 1e12 ? value : value * 1000;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'string') {
|
|
101
|
+
const parsed = Date.parse(value);
|
|
102
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function extractTextParts(value) {
|
|
107
|
+
if (typeof value === 'string') {
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
return trimmed ? [trimmed] : [];
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(value))
|
|
112
|
+
return [];
|
|
113
|
+
const parts = [];
|
|
114
|
+
for (const item of value) {
|
|
115
|
+
if (!item || typeof item !== 'object')
|
|
116
|
+
continue;
|
|
117
|
+
const block = item;
|
|
118
|
+
const type = typeof block.type === 'string' ? block.type : '';
|
|
119
|
+
if (type === 'text') {
|
|
120
|
+
const text = typeof block.text === 'string' ? block.text.trim() : '';
|
|
121
|
+
if (text)
|
|
122
|
+
parts.push(text);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (type === 'thinking') {
|
|
126
|
+
const thinking = typeof block.thinking === 'string' ? block.thinking.trim() : '';
|
|
127
|
+
if (thinking)
|
|
128
|
+
parts.push(thinking);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parts;
|
|
133
|
+
}
|
|
134
|
+
function extractAssistantArtifacts(lastAssistant) {
|
|
135
|
+
if (!lastAssistant || typeof lastAssistant !== 'object') {
|
|
136
|
+
return { texts: [], thinking: [] };
|
|
137
|
+
}
|
|
138
|
+
const source = lastAssistant;
|
|
139
|
+
const messageRecord = source.message && typeof source.message === 'object'
|
|
140
|
+
? source.message
|
|
141
|
+
: source;
|
|
142
|
+
const content = messageRecord.content;
|
|
143
|
+
if (!Array.isArray(content)) {
|
|
144
|
+
return {
|
|
145
|
+
texts: extractTextParts(content),
|
|
146
|
+
thinking: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const texts = [];
|
|
150
|
+
const thinking = [];
|
|
151
|
+
for (const item of content) {
|
|
152
|
+
if (!item || typeof item !== 'object')
|
|
153
|
+
continue;
|
|
154
|
+
const block = item;
|
|
155
|
+
const type = typeof block.type === 'string' ? block.type : '';
|
|
156
|
+
if (type === 'text') {
|
|
157
|
+
const text = typeof block.text === 'string' ? block.text.trim() : '';
|
|
158
|
+
if (text)
|
|
159
|
+
texts.push(text);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (type === 'thinking') {
|
|
163
|
+
const value = typeof block.thinking === 'string' ? block.thinking.trim() : '';
|
|
164
|
+
if (value)
|
|
165
|
+
thinking.push(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { texts, thinking };
|
|
169
|
+
}
|
|
170
|
+
function extractToolUseAssistantMessage(historyMessages) {
|
|
171
|
+
for (let i = historyMessages.length - 1; i >= 0; i--) {
|
|
172
|
+
const item = historyMessages[i];
|
|
173
|
+
if (!item || typeof item !== 'object')
|
|
174
|
+
continue;
|
|
175
|
+
const msg = item;
|
|
176
|
+
if (msg.role !== 'assistant')
|
|
177
|
+
continue;
|
|
178
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
179
|
+
if (content.length === 0)
|
|
180
|
+
continue;
|
|
181
|
+
const texts = [];
|
|
182
|
+
const thinking = [];
|
|
183
|
+
const toolCallIds = [];
|
|
184
|
+
let hasToolCall = false;
|
|
185
|
+
for (const blockValue of content) {
|
|
186
|
+
if (!blockValue || typeof blockValue !== 'object')
|
|
187
|
+
continue;
|
|
188
|
+
const block = blockValue;
|
|
189
|
+
const type = typeof block.type === 'string' ? block.type : '';
|
|
190
|
+
if (type === 'text') {
|
|
191
|
+
const text = typeof block.text === 'string' ? block.text.trim() : '';
|
|
192
|
+
if (text)
|
|
193
|
+
texts.push(text);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (type === 'thinking') {
|
|
197
|
+
const text = typeof block.thinking === 'string' ? block.thinking.trim() : '';
|
|
198
|
+
if (text)
|
|
199
|
+
thinking.push(text);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (type === 'toolCall') {
|
|
203
|
+
hasToolCall = true;
|
|
204
|
+
if (typeof block.id === 'string' && block.id.trim()) {
|
|
205
|
+
toolCallIds.push(block.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const stopReason = typeof msg.stopReason === 'string' ? msg.stopReason : undefined;
|
|
210
|
+
if (!hasToolCall && stopReason !== 'toolUse')
|
|
211
|
+
continue;
|
|
212
|
+
if (texts.length === 0 && thinking.length === 0)
|
|
213
|
+
continue;
|
|
214
|
+
return {
|
|
215
|
+
messageId: typeof msg.id === 'string' ? msg.id : undefined,
|
|
216
|
+
timestampMs: parseMessageTimestamp(msg.timestamp),
|
|
217
|
+
stopReason,
|
|
218
|
+
texts,
|
|
219
|
+
thinking,
|
|
220
|
+
toolCallIds,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function resolveSessionTranscriptPath(sessionId) {
|
|
226
|
+
if (!sessionId || sessionId === 'unknown')
|
|
227
|
+
return null;
|
|
228
|
+
if (sessionTranscriptPathCache.has(sessionId)) {
|
|
229
|
+
return sessionTranscriptPathCache.get(sessionId) ?? null;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const agentsRoot = path.join(os.homedir(), '.openclaw', 'agents');
|
|
233
|
+
const agentDirs = fs.readdirSync(agentsRoot, { withFileTypes: true });
|
|
234
|
+
for (const dir of agentDirs) {
|
|
235
|
+
if (!dir.isDirectory())
|
|
236
|
+
continue;
|
|
237
|
+
const candidate = path.join(agentsRoot, dir.name, 'sessions', `${sessionId}.jsonl`);
|
|
238
|
+
if (fs.existsSync(candidate)) {
|
|
239
|
+
sessionTranscriptPathCache.set(sessionId, candidate);
|
|
240
|
+
return candidate;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// ignore lookup failures
|
|
246
|
+
}
|
|
247
|
+
sessionTranscriptPathCache.set(sessionId, null);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function extractToolUseAssistantMessagesFromTranscript(params) {
|
|
251
|
+
const transcriptPath = resolveSessionTranscriptPath(params.sessionId);
|
|
252
|
+
if (!transcriptPath)
|
|
253
|
+
return [];
|
|
254
|
+
const lowerBound = (params.runStartedAt ?? params.runEndedAt - 300_000) - 2000;
|
|
255
|
+
const upperBound = params.runEndedAt + 2000;
|
|
256
|
+
let raw = '';
|
|
257
|
+
try {
|
|
258
|
+
raw = fs.readFileSync(transcriptPath, 'utf-8');
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const results = [];
|
|
264
|
+
const lines = raw.split('\n');
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (!line.trim())
|
|
267
|
+
continue;
|
|
268
|
+
let record;
|
|
269
|
+
try {
|
|
270
|
+
record = JSON.parse(line);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (record.type !== 'message')
|
|
276
|
+
continue;
|
|
277
|
+
const message = record.message;
|
|
278
|
+
if (!message || message.role !== 'assistant')
|
|
279
|
+
continue;
|
|
280
|
+
const timestampMs = parseMessageTimestamp(record.timestamp);
|
|
281
|
+
if (timestampMs === null || timestampMs < lowerBound || timestampMs > upperBound)
|
|
282
|
+
continue;
|
|
283
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
284
|
+
if (content.length === 0)
|
|
285
|
+
continue;
|
|
286
|
+
const texts = [];
|
|
287
|
+
const thinking = [];
|
|
288
|
+
const toolCallIds = [];
|
|
289
|
+
let hasToolCall = false;
|
|
290
|
+
for (const blockValue of content) {
|
|
291
|
+
if (!blockValue || typeof blockValue !== 'object')
|
|
292
|
+
continue;
|
|
293
|
+
const block = blockValue;
|
|
294
|
+
const type = typeof block.type === 'string' ? block.type : '';
|
|
295
|
+
if (type === 'text') {
|
|
296
|
+
const text = typeof block.text === 'string' ? block.text.trim() : '';
|
|
297
|
+
if (text)
|
|
298
|
+
texts.push(text);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (type === 'thinking') {
|
|
302
|
+
const text = typeof block.thinking === 'string' ? block.thinking.trim() : '';
|
|
303
|
+
if (text)
|
|
304
|
+
thinking.push(text);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (type === 'toolCall') {
|
|
308
|
+
hasToolCall = true;
|
|
309
|
+
if (typeof block.id === 'string' && block.id.trim()) {
|
|
310
|
+
toolCallIds.push(block.id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const stopReason = typeof message.stopReason === 'string' ? message.stopReason : undefined;
|
|
315
|
+
if (!hasToolCall && stopReason !== 'toolUse')
|
|
316
|
+
continue;
|
|
317
|
+
if (texts.length === 0 && thinking.length === 0)
|
|
318
|
+
continue;
|
|
319
|
+
const messageId = typeof record.id === 'string' ? record.id : `${timestampMs}:${toolCallIds.join(',')}`;
|
|
320
|
+
results.push({ messageId, timestampMs, stopReason, texts, thinking, toolCallIds });
|
|
321
|
+
}
|
|
322
|
+
results.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
53
325
|
function installFetchInterceptor() {
|
|
54
326
|
if (fetchInterceptorInstalled)
|
|
55
327
|
return;
|
|
@@ -142,7 +414,6 @@ function installFetchInterceptor() {
|
|
|
142
414
|
processSseLines();
|
|
143
415
|
if (extractedUsage) {
|
|
144
416
|
sseUsageCache.set(requestId, extractedUsage);
|
|
145
|
-
latestSseUsage = extractedUsage;
|
|
146
417
|
}
|
|
147
418
|
controller.close();
|
|
148
419
|
break;
|
|
@@ -175,11 +446,28 @@ function uninstallFetchInterceptor() {
|
|
|
175
446
|
}
|
|
176
447
|
}
|
|
177
448
|
/**
|
|
178
|
-
*
|
|
449
|
+
* Consume one SSE usage entry for a specific run.
|
|
450
|
+
* Picks the earliest entry whose timestamp is not older than run start
|
|
451
|
+
* (with small clock-tolerance). Falls back to FIFO oldest entry.
|
|
179
452
|
*/
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
453
|
+
function consumeSseUsageForRun(runStartedAt) {
|
|
454
|
+
if (sseUsageCache.size === 0)
|
|
455
|
+
return null;
|
|
456
|
+
const entries = Array.from(sseUsageCache.entries()).sort((a, b) => a[0] - b[0]);
|
|
457
|
+
let selectedKey = null;
|
|
458
|
+
if (typeof runStartedAt === 'number') {
|
|
459
|
+
const toleranceMs = 1000;
|
|
460
|
+
for (const [key, usage] of entries) {
|
|
461
|
+
if (usage.timestamp + toleranceMs >= runStartedAt) {
|
|
462
|
+
selectedKey = key;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (selectedKey === null)
|
|
468
|
+
selectedKey = entries[0][0];
|
|
469
|
+
const usage = sseUsageCache.get(selectedKey) ?? null;
|
|
470
|
+
sseUsageCache.delete(selectedKey);
|
|
183
471
|
// Also trim old cache entries
|
|
184
472
|
if (sseUsageCache.size > 100) {
|
|
185
473
|
const keys = Array.from(sseUsageCache.keys()).sort((a, b) => a - b);
|
|
@@ -190,14 +478,61 @@ function popLatestSseUsage() {
|
|
|
190
478
|
return usage;
|
|
191
479
|
}
|
|
192
480
|
const runInputs = new Map();
|
|
481
|
+
/** Track latest completed tool-call timestamp for each run (for thinking timeline alignment) */
|
|
482
|
+
const runLastToolCompletedAt = new Map();
|
|
483
|
+
/** Track latest assistant stream event timestamp for each run (for thinking end-time clamping) */
|
|
484
|
+
const runLastAssistantStreamAt = new Map();
|
|
485
|
+
/** Track first assistant stream event timestamp for each run (assistant start-time anchor) */
|
|
486
|
+
const runFirstAssistantStreamAt = new Map();
|
|
487
|
+
function summarizePromptMessages(messages) {
|
|
488
|
+
const roleCounts = {};
|
|
489
|
+
const firstRoles = [];
|
|
490
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
491
|
+
return { messageCount: 0, roleCounts, firstRoles };
|
|
492
|
+
}
|
|
493
|
+
for (let i = 0; i < messages.length; i++) {
|
|
494
|
+
const item = messages[i];
|
|
495
|
+
if (!item || typeof item !== 'object')
|
|
496
|
+
continue;
|
|
497
|
+
const role = String(item.role || 'unknown');
|
|
498
|
+
roleCounts[role] = (roleCounts[role] ?? 0) + 1;
|
|
499
|
+
if (firstRoles.length < 20) {
|
|
500
|
+
firstRoles.push(role);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
messageCount: messages.length,
|
|
505
|
+
roleCounts,
|
|
506
|
+
firstRoles,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function makeToolTimingKey(sessionId, runId, toolName, toolCallId) {
|
|
510
|
+
if (toolCallId)
|
|
511
|
+
return `tc:id:${toolCallId}`;
|
|
512
|
+
return `tc:fallback:${sessionId}:${runId ?? 'norun'}:${toolName}`;
|
|
513
|
+
}
|
|
193
514
|
/**
|
|
194
515
|
* Periodically clean up stale Map entries to prevent memory leaks when llm_input fires but llm_output does not
|
|
195
516
|
*/
|
|
196
517
|
function cleanupStaleMaps() {
|
|
197
518
|
const now = Date.now();
|
|
198
|
-
for (const [key, startTime] of
|
|
519
|
+
for (const [key, startTime] of llmRunStartTimes) {
|
|
199
520
|
if (now - startTime > MAP_ENTRY_TTL_MS) {
|
|
200
|
-
|
|
521
|
+
llmRunStartTimes.delete(key);
|
|
522
|
+
runSessionIds.delete(key);
|
|
523
|
+
runInputs.delete(key);
|
|
524
|
+
runLastToolCompletedAt.delete(key);
|
|
525
|
+
runLastAssistantStreamAt.delete(key);
|
|
526
|
+
runFirstAssistantStreamAt.delete(key);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const [key, queue] of toolCallStartTimes) {
|
|
530
|
+
const filtered = queue.filter((t) => now - t <= MAP_ENTRY_TTL_MS);
|
|
531
|
+
if (filtered.length === 0) {
|
|
532
|
+
toolCallStartTimes.delete(key);
|
|
533
|
+
}
|
|
534
|
+
else if (filtered.length !== queue.length) {
|
|
535
|
+
toolCallStartTimes.set(key, filtered);
|
|
201
536
|
}
|
|
202
537
|
}
|
|
203
538
|
// runInputs has no timestamps; trim oldest entries when exceeding size limit
|
|
@@ -230,6 +565,18 @@ function cleanupStaleMaps() {
|
|
|
230
565
|
sessionStatsMap.delete(entries[i][0]);
|
|
231
566
|
}
|
|
232
567
|
}
|
|
568
|
+
for (const [key, pending] of pendingRuntimeEvents) {
|
|
569
|
+
if (now - pending.lastEventAt > MAP_ENTRY_TTL_MS) {
|
|
570
|
+
if (pending.timer)
|
|
571
|
+
clearTimeout(pending.timer);
|
|
572
|
+
pendingRuntimeEvents.delete(key);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for (const [key, ts] of replayedAssistantMessages) {
|
|
576
|
+
if (now - ts > MAP_ENTRY_TTL_MS) {
|
|
577
|
+
replayedAssistantMessages.delete(key);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
233
580
|
}
|
|
234
581
|
/**
|
|
235
582
|
* Extract image/media metadata from historyMessages
|
|
@@ -446,6 +793,7 @@ let _alertBuffer = [];
|
|
|
446
793
|
let _alertFlushTimer = null;
|
|
447
794
|
let _mapCleanupTimer = null;
|
|
448
795
|
let _checkpointTimer = null;
|
|
796
|
+
let _unsubscribeAgentEvents = null;
|
|
449
797
|
function activate(api) {
|
|
450
798
|
const rawConfig = (api.pluginConfig || api.config || {});
|
|
451
799
|
const config = (0, config_1.resolveConfig)(rawConfig);
|
|
@@ -482,9 +830,38 @@ function activate(api) {
|
|
|
482
830
|
console.log('[openclaw-observability] Reusing existing writer/buffer (skipping duplicate init)');
|
|
483
831
|
}
|
|
484
832
|
const redactor = new redaction_1.Redactor(config.redaction);
|
|
485
|
-
|
|
486
|
-
|
|
833
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR ?? path.join(os.homedir(), '.openclaw');
|
|
834
|
+
// Security scanner (supports runtime rule toggles from UI)
|
|
835
|
+
const securityConfigPath = path.join(stateDir, 'data', 'observability-security-config.json');
|
|
836
|
+
let securityConfig = (0, scanner_1.resolveSecurityConfig)(config.security);
|
|
837
|
+
try {
|
|
838
|
+
if (fs.existsSync(securityConfigPath)) {
|
|
839
|
+
const raw = fs.readFileSync(securityConfigPath, 'utf8');
|
|
840
|
+
const parsed = JSON.parse(raw);
|
|
841
|
+
securityConfig = (0, scanner_1.resolveSecurityConfig)({
|
|
842
|
+
...securityConfig,
|
|
843
|
+
...parsed,
|
|
844
|
+
rules: {
|
|
845
|
+
...securityConfig.rules,
|
|
846
|
+
...(parsed.rules ?? {}),
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
console.log('[openclaw-observability-security] Loaded runtime security config from ' + securityConfigPath);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (e) {
|
|
853
|
+
console.warn('[openclaw-observability-security] Failed to load runtime security config:', e.message);
|
|
854
|
+
}
|
|
487
855
|
const securityScanner = new scanner_1.SecurityScanner(securityConfig);
|
|
856
|
+
function persistSecurityConfig(nextConfig) {
|
|
857
|
+
try {
|
|
858
|
+
fs.mkdirSync(path.dirname(securityConfigPath), { recursive: true });
|
|
859
|
+
fs.writeFileSync(securityConfigPath, JSON.stringify(nextConfig, null, 2), 'utf8');
|
|
860
|
+
}
|
|
861
|
+
catch (e) {
|
|
862
|
+
console.error('[openclaw-observability-security] Failed to persist runtime security config:', e);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
488
865
|
function startServices() {
|
|
489
866
|
if (_servicesStarted)
|
|
490
867
|
return;
|
|
@@ -530,15 +907,49 @@ function activate(api) {
|
|
|
530
907
|
'Please configure via Dashboard (Settings → Plugins → openclaw-observability Config) then restart.');
|
|
531
908
|
}
|
|
532
909
|
}
|
|
533
|
-
// Observability UI auth:
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
910
|
+
// Observability UI auth: read gateway.auth.token from same config file as gateway; if missing or empty, no auth (allow all)
|
|
911
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? path.join(stateDir, 'openclaw.json');
|
|
912
|
+
let observabilityToken;
|
|
913
|
+
try {
|
|
914
|
+
if (fs.existsSync(openclawConfigPath)) {
|
|
915
|
+
const raw = fs.readFileSync(openclawConfigPath, 'utf8');
|
|
916
|
+
let t;
|
|
917
|
+
try {
|
|
918
|
+
const parsed = JSON.parse(raw);
|
|
919
|
+
t = parsed?.gateway?.auth?.token;
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
// Config may be JSON5 (comments, unescaped newlines in strings); extract token with regex
|
|
923
|
+
const m = raw.match(/"gateway"\s*:\s*\{[\s\S]*?"auth"\s*:\s*\{[\s\S]*?"token"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
924
|
+
t = m?.[1]?.replace(/\\(.)/g, '$1');
|
|
925
|
+
}
|
|
926
|
+
if (typeof t === 'string' && t.length > 0)
|
|
927
|
+
observabilityToken = t;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
console.warn('[openclaw-observability] Failed to read gateway token from config:', e.message);
|
|
932
|
+
}
|
|
933
|
+
if (observabilityToken) {
|
|
934
|
+
console.log('[openclaw-observability] Observability UI auth: token required (from ' + openclawConfigPath + ')');
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
console.log('[openclaw-observability] Observability UI auth: no token, allow all');
|
|
938
|
+
}
|
|
539
939
|
// Always register HTTP routes on the current api (the latest api serves HTTP)
|
|
540
940
|
if (typeof api.registerHttpRoute === 'function') {
|
|
541
|
-
(0, routes_1.registerAuditRoutes)(api.registerHttpRoute.bind(api), writer, observabilityToken
|
|
941
|
+
(0, routes_1.registerAuditRoutes)(api.registerHttpRoute.bind(api), writer, observabilityToken, {
|
|
942
|
+
getConfig: () => securityScanner.getConfig(),
|
|
943
|
+
updateConfig: (patch) => {
|
|
944
|
+
const next = securityScanner.updateConfig(patch);
|
|
945
|
+
persistSecurityConfig(next);
|
|
946
|
+
console.log(`[openclaw-observability-security] Runtime config updated: enabled=${next.enabled} ` +
|
|
947
|
+
`secretLeakage=${next.rules.secretLeakage} highRiskOps=${next.rules.highRiskOps} dataExfiltration=${next.rules.dataExfiltration} ` +
|
|
948
|
+
`promptInjection=${next.rules.promptInjection} customRegex=${next.rules.customRegex} chainDetection=${next.rules.chainDetection} ` +
|
|
949
|
+
`customRegexRules=${next.customRegexRules.length}`);
|
|
950
|
+
return next;
|
|
951
|
+
},
|
|
952
|
+
});
|
|
542
953
|
}
|
|
543
954
|
else {
|
|
544
955
|
console.warn('[openclaw-observability] registerHttpRoute not available — observability UI disabled');
|
|
@@ -550,30 +961,28 @@ function activate(api) {
|
|
|
550
961
|
const session = updateSessionStats(action.sessionId, action.modelName, action.userId, tokens);
|
|
551
962
|
void buffer.addSession(session);
|
|
552
963
|
// --- Security scan ---
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
void flushAlerts();
|
|
571
|
-
}
|
|
964
|
+
try {
|
|
965
|
+
const alerts = securityScanner.scan(action);
|
|
966
|
+
if (alerts.length > 0) {
|
|
967
|
+
// Prevent _alertBuffer from growing unbounded
|
|
968
|
+
if (_alertBuffer.length + alerts.length > MAX_ALERT_BUFFER) {
|
|
969
|
+
const overflow = _alertBuffer.length + alerts.length - MAX_ALERT_BUFFER;
|
|
970
|
+
_alertBuffer.splice(0, overflow);
|
|
971
|
+
console.warn(`[openclaw-observability-security] Alert buffer overflow, dropped ${overflow} oldest alerts`);
|
|
972
|
+
}
|
|
973
|
+
_alertBuffer.push(...alerts);
|
|
974
|
+
for (const alert of alerts) {
|
|
975
|
+
const icon = alert.severity === 'critical' ? '🔴' : alert.severity === 'warn' ? '🟡' : 'ℹ️';
|
|
976
|
+
console.log(`[openclaw-observability-security] ${icon} ${alert.severity.toUpperCase()} ${alert.ruleId}: ${alert.ruleName} — ${alert.finding} (session=${alert.sessionId})`);
|
|
977
|
+
}
|
|
978
|
+
// Flush immediately on CRITICAL alerts
|
|
979
|
+
if (alerts.some(a => a.severity === 'critical')) {
|
|
980
|
+
void flushAlerts();
|
|
572
981
|
}
|
|
573
982
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
console.error('[openclaw-observability-security] Scan error:', err);
|
|
577
986
|
}
|
|
578
987
|
}
|
|
579
988
|
/** Batch-write security alerts */
|
|
@@ -590,12 +999,6 @@ function activate(api) {
|
|
|
590
999
|
_alertBuffer.unshift(...batch);
|
|
591
1000
|
}
|
|
592
1001
|
}
|
|
593
|
-
/** Safely truncate a string */
|
|
594
|
-
function truncate(s, max = 500) {
|
|
595
|
-
if (typeof s !== 'string')
|
|
596
|
-
return '';
|
|
597
|
-
return s.length > max ? s.substring(0, max) + '…' : s;
|
|
598
|
-
}
|
|
599
1002
|
/** Convert bytes to human-readable format */
|
|
600
1003
|
function formatBytes(bytes) {
|
|
601
1004
|
if (bytes < 1024)
|
|
@@ -604,6 +1007,141 @@ function activate(api) {
|
|
|
604
1007
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
605
1008
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
606
1009
|
}
|
|
1010
|
+
function flushPendingRuntimeEvent(key) {
|
|
1011
|
+
const pending = pendingRuntimeEvents.get(key);
|
|
1012
|
+
if (!pending)
|
|
1013
|
+
return;
|
|
1014
|
+
if (pending.timer) {
|
|
1015
|
+
clearTimeout(pending.timer);
|
|
1016
|
+
pending.timer = null;
|
|
1017
|
+
}
|
|
1018
|
+
pendingRuntimeEvents.delete(key);
|
|
1019
|
+
const text = pending.text.trim();
|
|
1020
|
+
if (!text)
|
|
1021
|
+
return;
|
|
1022
|
+
recordAction(makeAction(pending.sessionId, {
|
|
1023
|
+
actionType: pending.actionType,
|
|
1024
|
+
actionName: `${pending.stream}_stream`,
|
|
1025
|
+
inputParams: redactor.redact({
|
|
1026
|
+
runId: pending.runId,
|
|
1027
|
+
stream: pending.stream,
|
|
1028
|
+
}),
|
|
1029
|
+
outputResult: redactor.redact({
|
|
1030
|
+
text,
|
|
1031
|
+
length: text.length,
|
|
1032
|
+
}),
|
|
1033
|
+
durationMs: Math.max(0, pending.lastEventAt - pending.startedAt),
|
|
1034
|
+
createdAt: new Date(pending.lastEventAt),
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
function queueRuntimeTextEvent(params) {
|
|
1038
|
+
const key = `${params.runId}:${params.stream}`;
|
|
1039
|
+
const existing = pendingRuntimeEvents.get(key);
|
|
1040
|
+
if (existing) {
|
|
1041
|
+
existing.lastEventAt = params.ts;
|
|
1042
|
+
existing.text = params.text;
|
|
1043
|
+
if (existing.timer)
|
|
1044
|
+
clearTimeout(existing.timer);
|
|
1045
|
+
existing.timer = setTimeout(() => flushPendingRuntimeEvent(key), 900);
|
|
1046
|
+
if (existing.timer && typeof existing.timer === 'object' && 'unref' in existing.timer) {
|
|
1047
|
+
existing.timer.unref();
|
|
1048
|
+
}
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const timer = setTimeout(() => flushPendingRuntimeEvent(key), 900);
|
|
1052
|
+
if (timer && typeof timer === 'object' && 'unref' in timer) {
|
|
1053
|
+
timer.unref();
|
|
1054
|
+
}
|
|
1055
|
+
pendingRuntimeEvents.set(key, {
|
|
1056
|
+
actionType: params.actionType,
|
|
1057
|
+
sessionId: params.sessionId,
|
|
1058
|
+
runId: params.runId,
|
|
1059
|
+
stream: params.stream,
|
|
1060
|
+
startedAt: params.ts,
|
|
1061
|
+
lastEventAt: params.ts,
|
|
1062
|
+
text: params.text,
|
|
1063
|
+
timer,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function flushPendingRuntimeEventsForRun(runId) {
|
|
1067
|
+
const keys = Array.from(pendingRuntimeEvents.keys()).filter((key) => key.startsWith(`${runId}:`));
|
|
1068
|
+
keys.forEach((key) => flushPendingRuntimeEvent(key));
|
|
1069
|
+
}
|
|
1070
|
+
if (!_unsubscribeAgentEvents && typeof api.runtime?.events?.onAgentEvent === 'function') {
|
|
1071
|
+
const unsubscribe = api.runtime.events.onAgentEvent((evt) => {
|
|
1072
|
+
try {
|
|
1073
|
+
if (!evt?.runId || !evt?.stream)
|
|
1074
|
+
return;
|
|
1075
|
+
const sessionId = runSessionIds.get(evt.runId);
|
|
1076
|
+
if (!sessionId || sessionId === 'unknown')
|
|
1077
|
+
return;
|
|
1078
|
+
if (evt.stream === 'assistant' || evt.stream === 'thinking') {
|
|
1079
|
+
const fullText = typeof evt.data?.text === 'string'
|
|
1080
|
+
? evt.data.text
|
|
1081
|
+
: typeof evt.data?.delta === 'string'
|
|
1082
|
+
? evt.data.delta
|
|
1083
|
+
: '';
|
|
1084
|
+
if (!fullText.trim())
|
|
1085
|
+
return;
|
|
1086
|
+
runtimeTextSeen.add(getRuntimeTextSeenKey(evt.runId, evt.stream));
|
|
1087
|
+
if (evt.stream === 'assistant') {
|
|
1088
|
+
if (!runFirstAssistantStreamAt.has(evt.runId)) {
|
|
1089
|
+
runFirstAssistantStreamAt.set(evt.runId, typeof evt.ts === 'number' ? evt.ts : Date.now());
|
|
1090
|
+
}
|
|
1091
|
+
runLastAssistantStreamAt.set(evt.runId, typeof evt.ts === 'number' ? evt.ts : Date.now());
|
|
1092
|
+
}
|
|
1093
|
+
queueRuntimeTextEvent({
|
|
1094
|
+
sessionId,
|
|
1095
|
+
runId: evt.runId,
|
|
1096
|
+
stream: evt.stream,
|
|
1097
|
+
actionType: evt.stream === 'assistant' ? types_1.ActionType.AssistantStream : types_1.ActionType.Thinking,
|
|
1098
|
+
ts: typeof evt.ts === 'number' ? evt.ts : Date.now(),
|
|
1099
|
+
text: fullText,
|
|
1100
|
+
});
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (evt.stream === 'tool') {
|
|
1104
|
+
flushPendingRuntimeEventsForRun(evt.runId);
|
|
1105
|
+
const phase = typeof evt.data?.phase === 'string' ? evt.data.phase : '';
|
|
1106
|
+
const name = typeof evt.data?.name === 'string' ? evt.data.name : 'unknown';
|
|
1107
|
+
if (phase !== 'start' && phase !== 'update')
|
|
1108
|
+
return;
|
|
1109
|
+
const payload = { runId: evt.runId, phase };
|
|
1110
|
+
if (typeof evt.data?.toolCallId === 'string')
|
|
1111
|
+
payload.toolCallId = evt.data.toolCallId;
|
|
1112
|
+
if (phase === 'update' && evt.data?.partialResult !== undefined) {
|
|
1113
|
+
payload.partialResult = evt.data.partialResult;
|
|
1114
|
+
}
|
|
1115
|
+
recordAction(makeAction(sessionId, {
|
|
1116
|
+
actionType: types_1.ActionType.ToolUpdate,
|
|
1117
|
+
actionName: phase === 'start' ? `tool_start:${name}` : `tool_update:${name}`,
|
|
1118
|
+
inputParams: redactor.redact(payload),
|
|
1119
|
+
createdAt: new Date(typeof evt.ts === 'number' ? evt.ts : Date.now()),
|
|
1120
|
+
}));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (evt.stream === 'lifecycle') {
|
|
1124
|
+
const phase = typeof evt.data?.phase === 'string' ? evt.data.phase : '';
|
|
1125
|
+
if (phase === 'end' || phase === 'error') {
|
|
1126
|
+
flushPendingRuntimeEventsForRun(evt.runId);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
catch (err) {
|
|
1131
|
+
console.error('[openclaw-observability] Error handling runtime agent event:', err);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
if (typeof unsubscribe === 'function') {
|
|
1135
|
+
_unsubscribeAgentEvents = unsubscribe;
|
|
1136
|
+
}
|
|
1137
|
+
console.log('[openclaw-observability] Runtime agent event listener attached');
|
|
1138
|
+
}
|
|
1139
|
+
else if (_unsubscribeAgentEvents) {
|
|
1140
|
+
console.log('[openclaw-observability] Reusing existing runtime agent event listener');
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
console.log('[openclaw-observability] Runtime agent events unavailable on this OpenClaw build');
|
|
1144
|
+
}
|
|
607
1145
|
// =====================================================================
|
|
608
1146
|
// 1. Agent lifecycle
|
|
609
1147
|
// =====================================================================
|
|
@@ -620,7 +1158,7 @@ function activate(api) {
|
|
|
620
1158
|
actionType: types_1.ActionType.ModelResolve,
|
|
621
1159
|
actionName: 'before_model_resolve',
|
|
622
1160
|
userId: c?.agentId,
|
|
623
|
-
inputParams: redactor.redact({ prompt:
|
|
1161
|
+
inputParams: redactor.redact({ prompt: e.prompt }),
|
|
624
1162
|
}));
|
|
625
1163
|
console.log(`[openclaw-observability] before_model_resolve: session=${sid}`);
|
|
626
1164
|
}
|
|
@@ -634,13 +1172,16 @@ function activate(api) {
|
|
|
634
1172
|
const e = event;
|
|
635
1173
|
const c = ctx;
|
|
636
1174
|
const sid = resolveSessionId(undefined, c?.sessionId);
|
|
1175
|
+
const summary = summarizePromptMessages(e.messages);
|
|
637
1176
|
recordAction(makeAction(sid, {
|
|
638
1177
|
actionType: types_1.ActionType.PromptBuild,
|
|
639
1178
|
actionName: 'before_prompt_build',
|
|
640
1179
|
userId: c?.agentId,
|
|
641
1180
|
inputParams: redactor.redact({
|
|
642
|
-
prompt:
|
|
643
|
-
messageCount:
|
|
1181
|
+
prompt: e.prompt,
|
|
1182
|
+
messageCount: summary.messageCount,
|
|
1183
|
+
messageRoleCounts: summary.roleCounts,
|
|
1184
|
+
firstMessageRoles: summary.firstRoles,
|
|
644
1185
|
}),
|
|
645
1186
|
}));
|
|
646
1187
|
console.log(`[openclaw-observability] before_prompt_build: session=${sid} msgs=${Array.isArray(e.messages) ? e.messages.length : '?'}`);
|
|
@@ -686,10 +1227,46 @@ function activate(api) {
|
|
|
686
1227
|
const sctx = getSessionCtx(sid);
|
|
687
1228
|
// Parse image/media metadata from historyMessages
|
|
688
1229
|
const media = extractMediaMeta(e.historyMessages ?? []);
|
|
1230
|
+
const modelName = `${e.provider}/${e.model}`;
|
|
1231
|
+
const priorAssistant = extractToolUseAssistantMessage(e.historyMessages ?? []);
|
|
1232
|
+
if (priorAssistant && e.runId) {
|
|
1233
|
+
const replayKey = [
|
|
1234
|
+
sid,
|
|
1235
|
+
e.runId,
|
|
1236
|
+
priorAssistant.messageId ?? String(priorAssistant.timestampMs ?? 'notime'),
|
|
1237
|
+
priorAssistant.toolCallIds.join(','),
|
|
1238
|
+
].join(':');
|
|
1239
|
+
if (!replayedAssistantMessages.has(replayKey)) {
|
|
1240
|
+
replayedAssistantMessages.set(replayKey, Date.now());
|
|
1241
|
+
const replayAt = new Date(priorAssistant.timestampMs ?? Math.max(Date.now() - 5, 0));
|
|
1242
|
+
const replayMeta = {
|
|
1243
|
+
runId: e.runId,
|
|
1244
|
+
source: 'history_message',
|
|
1245
|
+
synthetic: true,
|
|
1246
|
+
stopReason: priorAssistant.stopReason ?? 'toolUse',
|
|
1247
|
+
messageId: priorAssistant.messageId,
|
|
1248
|
+
toolCallIds: priorAssistant.toolCallIds,
|
|
1249
|
+
};
|
|
1250
|
+
if (priorAssistant.texts.length > 0) {
|
|
1251
|
+
const assistantText = priorAssistant.texts.join('\n\n');
|
|
1252
|
+
recordAction(makeAction(sid, {
|
|
1253
|
+
actionType: types_1.ActionType.AssistantStream,
|
|
1254
|
+
actionName: 'assistant_stream',
|
|
1255
|
+
modelName,
|
|
1256
|
+
inputParams: redactor.redact(replayMeta),
|
|
1257
|
+
outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
|
|
1258
|
+
createdAt: new Date(replayAt.getTime() + 1),
|
|
1259
|
+
userId: c?.agentId,
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
689
1264
|
if (e.runId) {
|
|
690
|
-
|
|
1265
|
+
llmRunStartTimes.set(e.runId, Date.now());
|
|
1266
|
+
runSessionIds.set(e.runId, sid);
|
|
691
1267
|
runInputs.set(e.runId, {
|
|
692
1268
|
prompt: e.prompt,
|
|
1269
|
+
systemPrompt: typeof e.systemPrompt === 'string' ? e.systemPrompt : undefined,
|
|
693
1270
|
imagesCount: e.imagesCount,
|
|
694
1271
|
media: media.length > 0 ? media : undefined,
|
|
695
1272
|
});
|
|
@@ -701,7 +1278,7 @@ function activate(api) {
|
|
|
701
1278
|
saveChannelToContext(sid, identifiedChannel);
|
|
702
1279
|
// Use identified channel or existing context channel
|
|
703
1280
|
const channelId = identifiedChannel !== 'webchat' ? identifiedChannel : (sctx.channelId || 'webchat');
|
|
704
|
-
sctx.modelName =
|
|
1281
|
+
sctx.modelName = modelName;
|
|
705
1282
|
console.log(`[openclaw-observability] llm_input: session=${sid} model=${e.provider}/${e.model} runId=${e.runId} channel=${channelId} images=${e.imagesCount ?? 0} mediaParts=${media.length}`);
|
|
706
1283
|
}
|
|
707
1284
|
catch (err) {
|
|
@@ -715,9 +1292,12 @@ function activate(api) {
|
|
|
715
1292
|
const c = ctx;
|
|
716
1293
|
// Calculate duration
|
|
717
1294
|
let durationMs = null;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1295
|
+
const runStartedAt = e.runId ? llmRunStartTimes.get(e.runId) : undefined;
|
|
1296
|
+
const runLastToolAt = e.runId ? runLastToolCompletedAt.get(e.runId) : undefined;
|
|
1297
|
+
const runAssistantAt = e.runId ? runLastAssistantStreamAt.get(e.runId) : undefined;
|
|
1298
|
+
const runAssistantFirstAt = e.runId ? runFirstAssistantStreamAt.get(e.runId) : undefined;
|
|
1299
|
+
if (typeof runStartedAt === 'number') {
|
|
1300
|
+
durationMs = Date.now() - runStartedAt;
|
|
721
1301
|
}
|
|
722
1302
|
// Retrieve cached user input
|
|
723
1303
|
const cachedInput = e.runId ? runInputs.get(e.runId) : undefined;
|
|
@@ -748,7 +1328,7 @@ function activate(api) {
|
|
|
748
1328
|
}
|
|
749
1329
|
// Strategy 3: SSE usage captured by fetch interceptor (stream_options injection)
|
|
750
1330
|
if (promptTokens === null && completionTokens === null) {
|
|
751
|
-
const sseUsage =
|
|
1331
|
+
const sseUsage = consumeSseUsageForRun(runStartedAt);
|
|
752
1332
|
if (sseUsage) {
|
|
753
1333
|
promptTokens = sseUsage.input || null;
|
|
754
1334
|
completionTokens = sseUsage.output || null;
|
|
@@ -758,11 +1338,22 @@ function activate(api) {
|
|
|
758
1338
|
}
|
|
759
1339
|
const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
|
|
760
1340
|
const modelName = `${e.provider}/${e.model}`;
|
|
1341
|
+
const fallbackArtifacts = extractAssistantArtifacts(e.lastAssistant);
|
|
1342
|
+
const assistantTexts = Array.isArray(e.assistantTexts) && e.assistantTexts.some((text) => typeof text === 'string' && text.trim())
|
|
1343
|
+
? e.assistantTexts.filter((text) => typeof text === 'string' && text.trim().length > 0)
|
|
1344
|
+
: fallbackArtifacts.texts;
|
|
761
1345
|
// Build inputParams with media metadata
|
|
762
1346
|
const inputData = {
|
|
1347
|
+
runId: e.runId,
|
|
763
1348
|
userMessage: cachedInput?.prompt ?? '',
|
|
764
1349
|
imagesCount: cachedInput?.imagesCount ?? 0,
|
|
765
1350
|
};
|
|
1351
|
+
if (typeof cachedInput?.systemPrompt === 'string' && cachedInput.systemPrompt.length > 0) {
|
|
1352
|
+
inputData.systemPrompt = cachedInput.systemPrompt;
|
|
1353
|
+
inputData.systemPromptHash = (0, node_crypto_1.createHash)('sha256')
|
|
1354
|
+
.update(cachedInput.systemPrompt, 'utf8')
|
|
1355
|
+
.digest('hex');
|
|
1356
|
+
}
|
|
766
1357
|
if (cachedInput?.media && cachedInput.media.length > 0) {
|
|
767
1358
|
inputData.media = cachedInput.media.map((m) => ({
|
|
768
1359
|
mimeType: m.mimeType,
|
|
@@ -782,14 +1373,127 @@ function activate(api) {
|
|
|
782
1373
|
modelName,
|
|
783
1374
|
inputParams: redactor.redact(inputData),
|
|
784
1375
|
outputResult: redactor.redact({
|
|
785
|
-
|
|
1376
|
+
runId: e.runId,
|
|
1377
|
+
assistantTexts,
|
|
1378
|
+
thinking: fallbackArtifacts.thinking,
|
|
1379
|
+
usage: {
|
|
1380
|
+
promptTokens,
|
|
1381
|
+
completionTokens,
|
|
1382
|
+
totalTokens,
|
|
1383
|
+
cacheRead,
|
|
1384
|
+
cacheWrite,
|
|
1385
|
+
},
|
|
786
1386
|
}),
|
|
787
1387
|
promptTokens,
|
|
788
1388
|
completionTokens,
|
|
789
1389
|
durationMs,
|
|
790
1390
|
userId: c?.agentId,
|
|
791
1391
|
}), totalTokens);
|
|
1392
|
+
const endedAt = Date.now();
|
|
1393
|
+
const syntheticTs = endedAt;
|
|
1394
|
+
if (e.runId) {
|
|
1395
|
+
const transcriptToolUseMessages = extractToolUseAssistantMessagesFromTranscript({
|
|
1396
|
+
sessionId: sid,
|
|
1397
|
+
runStartedAt,
|
|
1398
|
+
runEndedAt: endedAt,
|
|
1399
|
+
});
|
|
1400
|
+
for (const msg of transcriptToolUseMessages) {
|
|
1401
|
+
const replayKey = `${sid}:${e.runId}:${msg.messageId}`;
|
|
1402
|
+
if (replayedAssistantMessages.has(replayKey))
|
|
1403
|
+
continue;
|
|
1404
|
+
replayedAssistantMessages.set(replayKey, Date.now());
|
|
1405
|
+
const replayMeta = {
|
|
1406
|
+
runId: e.runId,
|
|
1407
|
+
source: 'transcript_message',
|
|
1408
|
+
synthetic: true,
|
|
1409
|
+
stopReason: msg.stopReason ?? 'toolUse',
|
|
1410
|
+
messageId: msg.messageId,
|
|
1411
|
+
toolCallIds: msg.toolCallIds,
|
|
1412
|
+
};
|
|
1413
|
+
const replayAt = new Date(msg.timestampMs);
|
|
1414
|
+
if (msg.thinking.length > 0) {
|
|
1415
|
+
const thinkingText = msg.thinking.join('\n\n');
|
|
1416
|
+
recordAction(makeAction(sid, {
|
|
1417
|
+
actionType: types_1.ActionType.Thinking,
|
|
1418
|
+
actionName: 'thinking',
|
|
1419
|
+
modelName,
|
|
1420
|
+
inputParams: redactor.redact(replayMeta),
|
|
1421
|
+
outputResult: redactor.redact({ text: thinkingText, length: thinkingText.length }),
|
|
1422
|
+
createdAt: replayAt,
|
|
1423
|
+
userId: c?.agentId,
|
|
1424
|
+
}));
|
|
1425
|
+
}
|
|
1426
|
+
if (msg.texts.length > 0) {
|
|
1427
|
+
const assistantText = msg.texts.join('\n\n');
|
|
1428
|
+
recordAction(makeAction(sid, {
|
|
1429
|
+
actionType: types_1.ActionType.AssistantStream,
|
|
1430
|
+
actionName: 'assistant_stream',
|
|
1431
|
+
modelName,
|
|
1432
|
+
inputParams: redactor.redact(replayMeta),
|
|
1433
|
+
outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
|
|
1434
|
+
createdAt: new Date(msg.timestampMs + 1),
|
|
1435
|
+
userId: c?.agentId,
|
|
1436
|
+
}));
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (assistantTexts.length > 0 &&
|
|
1440
|
+
!runtimeTextSeen.has(getRuntimeTextSeenKey(e.runId, 'assistant'))) {
|
|
1441
|
+
var assistantText = assistantTexts.join('\n\n');
|
|
1442
|
+
recordAction(makeAction(sid, {
|
|
1443
|
+
actionType: types_1.ActionType.AssistantStream,
|
|
1444
|
+
actionName: 'assistant_stream',
|
|
1445
|
+
modelName,
|
|
1446
|
+
inputParams: redactor.redact({ runId: e.runId, stream: 'assistant', synthetic: true }),
|
|
1447
|
+
outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
|
|
1448
|
+
createdAt: new Date(syntheticTs - 1),
|
|
1449
|
+
userId: c?.agentId,
|
|
1450
|
+
}));
|
|
1451
|
+
}
|
|
1452
|
+
if (fallbackArtifacts.thinking.length > 0 &&
|
|
1453
|
+
!runtimeTextSeen.has(getRuntimeTextSeenKey(e.runId, 'thinking'))) {
|
|
1454
|
+
const thinkingText = fallbackArtifacts.thinking.join('\n\n');
|
|
1455
|
+
let thinkingEndAt = Math.max(syntheticTs - 2, 0);
|
|
1456
|
+
// Anchor thinking end at assistant start time when available.
|
|
1457
|
+
// Priority: pending assistant stream start -> first observed assistant stream ts.
|
|
1458
|
+
const pendingAssistant = e.runId
|
|
1459
|
+
? pendingRuntimeEvents.get(`${e.runId}:assistant`)
|
|
1460
|
+
: undefined;
|
|
1461
|
+
const assistantStartAt = (pendingAssistant && typeof pendingAssistant.startedAt === 'number')
|
|
1462
|
+
? pendingAssistant.startedAt
|
|
1463
|
+
: (typeof runAssistantFirstAt === 'number' ? runAssistantFirstAt : undefined);
|
|
1464
|
+
if (typeof assistantStartAt === 'number') {
|
|
1465
|
+
thinkingEndAt = Math.min(thinkingEndAt, Math.max(assistantStartAt, 0));
|
|
1466
|
+
}
|
|
1467
|
+
else if (typeof runAssistantAt === 'number') {
|
|
1468
|
+
thinkingEndAt = Math.min(thinkingEndAt, Math.max(runAssistantAt - 1, 0));
|
|
1469
|
+
}
|
|
1470
|
+
let inferredThinkingDuration = null;
|
|
1471
|
+
if (typeof runLastToolAt === 'number' && runLastToolAt < thinkingEndAt) {
|
|
1472
|
+
inferredThinkingDuration = Math.max(1, thinkingEndAt - runLastToolAt);
|
|
1473
|
+
}
|
|
1474
|
+
recordAction(makeAction(sid, {
|
|
1475
|
+
actionType: types_1.ActionType.Thinking,
|
|
1476
|
+
actionName: 'thinking',
|
|
1477
|
+
modelName,
|
|
1478
|
+
inputParams: redactor.redact({ runId: e.runId, stream: 'thinking', synthetic: true }),
|
|
1479
|
+
outputResult: redactor.redact({ text: thinkingText, length: thinkingText.length }),
|
|
1480
|
+
createdAt: new Date(thinkingEndAt),
|
|
1481
|
+
durationMs: inferredThinkingDuration,
|
|
1482
|
+
userId: c?.agentId,
|
|
1483
|
+
}));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
792
1486
|
console.log(`[openclaw-observability] llm_output: session=${e.sessionId} model=${e.model} duration=${durationMs}ms tokens=${promptTokens ?? '?'}/${completionTokens ?? '?'} cache_r=${cacheRead ?? '-'} cache_w=${cacheWrite ?? '-'}`);
|
|
1487
|
+
if (e.runId) {
|
|
1488
|
+
flushPendingRuntimeEventsForRun(e.runId);
|
|
1489
|
+
llmRunStartTimes.delete(e.runId);
|
|
1490
|
+
runSessionIds.delete(e.runId);
|
|
1491
|
+
runLastToolCompletedAt.delete(e.runId);
|
|
1492
|
+
runLastAssistantStreamAt.delete(e.runId);
|
|
1493
|
+
runFirstAssistantStreamAt.delete(e.runId);
|
|
1494
|
+
runtimeTextSeen.delete(getRuntimeTextSeenKey(e.runId, 'assistant'));
|
|
1495
|
+
runtimeTextSeen.delete(getRuntimeTextSeenKey(e.runId, 'thinking'));
|
|
1496
|
+
}
|
|
793
1497
|
}
|
|
794
1498
|
catch (err) {
|
|
795
1499
|
console.error('[openclaw-observability] Error in llm_output:', err);
|
|
@@ -913,8 +1617,9 @@ function activate(api) {
|
|
|
913
1617
|
modelName: '',
|
|
914
1618
|
inputParams: redactor.redact({
|
|
915
1619
|
to: e.to,
|
|
916
|
-
content:
|
|
1620
|
+
content: e.content,
|
|
917
1621
|
channelId: c?.channelId,
|
|
1622
|
+
metadata: e.metadata,
|
|
918
1623
|
}),
|
|
919
1624
|
}));
|
|
920
1625
|
console.log(`[openclaw-observability] message_sending: to=${e.to} channel=${c?.channelId}`);
|
|
@@ -958,13 +1663,14 @@ function activate(api) {
|
|
|
958
1663
|
try {
|
|
959
1664
|
const e = event;
|
|
960
1665
|
const c = ctx;
|
|
961
|
-
// Use
|
|
962
|
-
// Note: cannot use Date.now() as fallback, otherwise before and after generate inconsistent keys
|
|
1666
|
+
// Use a stable key; when toolCallId is missing, use session/run/tool fallback key.
|
|
963
1667
|
const callId = e.toolCallId || c?.toolCallId || '';
|
|
964
|
-
const timingKey = callId ? `tc:${callId}` : `tc:${e.toolName}`;
|
|
965
|
-
runStartTimes.set(timingKey, Date.now());
|
|
966
|
-
// Update per-session context
|
|
967
1668
|
const sid = resolveSessionId(undefined, c?.sessionId);
|
|
1669
|
+
const timingKey = makeToolTimingKey(sid, c?.runId || e.runId, e.toolName, callId || undefined);
|
|
1670
|
+
const queue = toolCallStartTimes.get(timingKey) ?? [];
|
|
1671
|
+
queue.push(Date.now());
|
|
1672
|
+
toolCallStartTimes.set(timingKey, queue);
|
|
1673
|
+
// Update per-session context
|
|
968
1674
|
const sctx = getSessionCtx(sid);
|
|
969
1675
|
if (c?.agentId)
|
|
970
1676
|
sctx.userId = c.agentId;
|
|
@@ -984,14 +1690,29 @@ function activate(api) {
|
|
|
984
1690
|
const sessionId = resolveSessionId(undefined, c?.sessionId);
|
|
985
1691
|
// Calculate duration — use toolCallId as key to not interfere with LLM runId timing
|
|
986
1692
|
const callId = e.toolCallId || c?.toolCallId || '';
|
|
987
|
-
const timingKey =
|
|
1693
|
+
const timingKey = makeToolTimingKey(sessionId, c?.runId || e.runId, e.toolName, callId || undefined);
|
|
988
1694
|
let durationMs = e.durationMs ?? null;
|
|
989
|
-
|
|
990
|
-
|
|
1695
|
+
const queue = toolCallStartTimes.get(timingKey);
|
|
1696
|
+
const startAt = queue && queue.length > 0 ? queue.shift() : undefined;
|
|
1697
|
+
if (queue && queue.length > 0) {
|
|
1698
|
+
toolCallStartTimes.set(timingKey, queue);
|
|
1699
|
+
}
|
|
1700
|
+
else {
|
|
1701
|
+
toolCallStartTimes.delete(timingKey);
|
|
1702
|
+
}
|
|
1703
|
+
if (durationMs === null && typeof startAt === 'number') {
|
|
1704
|
+
durationMs = Date.now() - startAt;
|
|
1705
|
+
}
|
|
1706
|
+
if (typeof (e.runId || c?.runId) === 'string') {
|
|
1707
|
+
runLastToolCompletedAt.set(String(e.runId || c?.runId), Date.now());
|
|
991
1708
|
}
|
|
992
|
-
runStartTimes.delete(timingKey);
|
|
993
1709
|
// BUG FIX: use correct field name params (not parameters/input/args)
|
|
994
|
-
const
|
|
1710
|
+
const rawInputParams = e.params ? { ...e.params } : {};
|
|
1711
|
+
if (callId)
|
|
1712
|
+
rawInputParams.toolCallId = callId;
|
|
1713
|
+
if (e.runId || c?.runId)
|
|
1714
|
+
rawInputParams.runId = e.runId || c?.runId;
|
|
1715
|
+
const inputParams = redactor.redact(rawInputParams);
|
|
995
1716
|
// Detect image content in tool results
|
|
996
1717
|
let outputData = null;
|
|
997
1718
|
if (e.error) {
|
|
@@ -1055,29 +1776,6 @@ function activate(api) {
|
|
|
1055
1776
|
console.error('[openclaw-observability] Error in after_tool_call:', err);
|
|
1056
1777
|
}
|
|
1057
1778
|
});
|
|
1058
|
-
// tool_result_persist: tool result persistence (sync hook)
|
|
1059
|
-
api.on('tool_result_persist', (event, ctx) => {
|
|
1060
|
-
try {
|
|
1061
|
-
const e = event;
|
|
1062
|
-
const c = ctx;
|
|
1063
|
-
const name = e.toolName || c?.toolName || 'unknown';
|
|
1064
|
-
const sid = lastFallbackSessionId;
|
|
1065
|
-
if (sid === 'unknown')
|
|
1066
|
-
return;
|
|
1067
|
-
recordAction(makeAction(sid, {
|
|
1068
|
-
actionType: types_1.ActionType.ToolPersist,
|
|
1069
|
-
actionName: `tool_persist:${name}`,
|
|
1070
|
-
inputParams: redactor.redact({
|
|
1071
|
-
toolName: name,
|
|
1072
|
-
toolCallId: e.toolCallId,
|
|
1073
|
-
isSynthetic: e.isSynthetic,
|
|
1074
|
-
}),
|
|
1075
|
-
}));
|
|
1076
|
-
}
|
|
1077
|
-
catch (err) {
|
|
1078
|
-
console.error('[openclaw-observability] Error in tool_result_persist:', err);
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
1779
|
// =====================================================================
|
|
1082
1780
|
// 6. Session lifecycle
|
|
1083
1781
|
// =====================================================================
|
|
@@ -1181,6 +1879,8 @@ function activate(api) {
|
|
|
1181
1879
|
reason: e.reason,
|
|
1182
1880
|
outcome: e.outcome,
|
|
1183
1881
|
error: e.error,
|
|
1882
|
+
runId: e.runId,
|
|
1883
|
+
endedAt: e.endedAt,
|
|
1184
1884
|
}),
|
|
1185
1885
|
}));
|
|
1186
1886
|
console.log(`[openclaw-observability] subagent_ended: target=${e.targetSessionKey} outcome=${e.outcome}`);
|
|
@@ -1240,6 +1940,11 @@ function activate(api) {
|
|
|
1240
1940
|
return {
|
|
1241
1941
|
deactivate: async () => {
|
|
1242
1942
|
console.log('[openclaw-observability] Deactivating plugin...');
|
|
1943
|
+
if (_unsubscribeAgentEvents) {
|
|
1944
|
+
_unsubscribeAgentEvents();
|
|
1945
|
+
_unsubscribeAgentEvents = null;
|
|
1946
|
+
}
|
|
1947
|
+
Array.from(pendingRuntimeEvents.keys()).forEach((key) => flushPendingRuntimeEvent(key));
|
|
1243
1948
|
// Stop module-level timers
|
|
1244
1949
|
if (_alertFlushTimer) {
|
|
1245
1950
|
clearInterval(_alertFlushTimer);
|
|
@@ -1268,10 +1973,12 @@ function activate(api) {
|
|
|
1268
1973
|
_alertBuffer = [];
|
|
1269
1974
|
_servicesStarted = false;
|
|
1270
1975
|
// Reset function-level state
|
|
1271
|
-
|
|
1976
|
+
llmRunStartTimes.clear();
|
|
1977
|
+
runSessionIds.clear();
|
|
1978
|
+
toolCallStartTimes.clear();
|
|
1272
1979
|
runInputs.clear();
|
|
1980
|
+
pendingRuntimeEvents.clear();
|
|
1273
1981
|
sseUsageCache.clear();
|
|
1274
|
-
latestSseUsage = null;
|
|
1275
1982
|
sessionStatsMap.clear();
|
|
1276
1983
|
sessionContextMap.clear();
|
|
1277
1984
|
securityScanner.reset();
|