@useorgx/openclaw-plugin 0.2.0 → 0.3.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 +16 -1
- package/dashboard/dist/assets/index-BrAP-X_H.css +1 -0
- package/dashboard/dist/assets/index-cOk6qwh-.js +56 -0
- package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg +10 -0
- package/dashboard/dist/brand/control-tower.png +0 -0
- package/dashboard/dist/brand/design-codex.png +0 -0
- package/dashboard/dist/brand/engineering-autopilot.png +0 -0
- package/dashboard/dist/brand/launch-captain.png +0 -0
- package/dashboard/dist/brand/openai-mark.svg +10 -0
- package/dashboard/dist/brand/openclaw-mark.svg +11 -0
- package/dashboard/dist/brand/orgx-logo.png +0 -0
- package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
- package/dashboard/dist/brand/product-orchestrator.png +0 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/api.d.ts +51 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +105 -15
- package/dist/api.js.map +1 -1
- package/dist/auth-store.d.ts +20 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +128 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/dashboard-api.d.ts +2 -7
- package/dist/dashboard-api.d.ts.map +1 -1
- package/dist/dashboard-api.js +2 -4
- package/dist/dashboard-api.js.map +1 -1
- package/dist/http-handler.d.ts +32 -3
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +1849 -35
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1453 -44
- package/dist/index.js.map +1 -1
- package/dist/local-openclaw.d.ts +87 -0
- package/dist/local-openclaw.d.ts.map +1 -0
- package/dist/local-openclaw.js +774 -0
- package/dist/local-openclaw.js.map +1 -0
- package/dist/openclaw.plugin.json +76 -0
- package/dist/outbox.d.ts +20 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +86 -0
- package/dist/outbox.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +4 -2
- package/skills/orgx/SKILL.md +180 -0
- package/dashboard/dist/assets/index-B_ag4FNd.css +0 -1
- package/dashboard/dist/assets/index-CNJpL8Wo.js +0 -40
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
const ACTIVE_WINDOW_MS = 30 * 60_000;
|
|
5
|
+
const LOCAL_SNAPSHOT_CACHE_TTL_MS = 1_500;
|
|
6
|
+
let localSnapshotCache = null;
|
|
7
|
+
async function readJsonFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(path, "utf8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function toIsoString(value) {
|
|
17
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
18
|
+
return { iso: new Date(value).toISOString(), ms: value };
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
const parsed = Date.parse(value);
|
|
22
|
+
if (Number.isFinite(parsed)) {
|
|
23
|
+
return { iso: new Date(parsed).toISOString(), ms: parsed };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { iso: null, ms: 0 };
|
|
27
|
+
}
|
|
28
|
+
function parseAgentId(sessionKey) {
|
|
29
|
+
const match = /^agent:([^:]+):/.exec(sessionKey);
|
|
30
|
+
return match?.[1] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function parseSessionLabel(sessionKey) {
|
|
33
|
+
const parts = sessionKey.split(":");
|
|
34
|
+
if (parts.length >= 3 && parts[0] === "agent") {
|
|
35
|
+
return parts.slice(2).join(":") || sessionKey;
|
|
36
|
+
}
|
|
37
|
+
return sessionKey;
|
|
38
|
+
}
|
|
39
|
+
function resolveDefaultAgentId(config) {
|
|
40
|
+
const list = config?.agents?.list;
|
|
41
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
42
|
+
const preferred = list.find((entry) => entry.default && typeof entry.id === "string");
|
|
43
|
+
if (preferred?.id)
|
|
44
|
+
return preferred.id;
|
|
45
|
+
const first = list.find((entry) => typeof entry.id === "string");
|
|
46
|
+
if (first?.id)
|
|
47
|
+
return first.id;
|
|
48
|
+
}
|
|
49
|
+
return "main";
|
|
50
|
+
}
|
|
51
|
+
function determineAgentStatus(session, nowMs) {
|
|
52
|
+
if (!session)
|
|
53
|
+
return "idle";
|
|
54
|
+
if (session.abortedLastRun && nowMs - session.updatedAtMs <= 24 * 60 * 60_000) {
|
|
55
|
+
return "blocked";
|
|
56
|
+
}
|
|
57
|
+
if (session.updatedAtMs > 0 && nowMs - session.updatedAtMs <= ACTIVE_WINDOW_MS) {
|
|
58
|
+
return "active";
|
|
59
|
+
}
|
|
60
|
+
return "idle";
|
|
61
|
+
}
|
|
62
|
+
function normalizeSessions(input, configuredAgents) {
|
|
63
|
+
if (!input || typeof input !== "object")
|
|
64
|
+
return [];
|
|
65
|
+
const sessions = [];
|
|
66
|
+
for (const [key, record] of Object.entries(input)) {
|
|
67
|
+
if (!record || typeof record !== "object")
|
|
68
|
+
continue;
|
|
69
|
+
const { iso, ms } = toIsoString(record.updatedAt);
|
|
70
|
+
const agentId = parseAgentId(key);
|
|
71
|
+
const displayName = typeof record.displayName === "string" && record.displayName.trim().length > 0
|
|
72
|
+
? record.displayName.trim()
|
|
73
|
+
: typeof record.origin?.label === "string" && record.origin.label.trim().length > 0
|
|
74
|
+
? record.origin.label.trim()
|
|
75
|
+
: parseSessionLabel(key);
|
|
76
|
+
const agentName = agentId ? configuredAgents.get(agentId) ?? agentId : null;
|
|
77
|
+
sessions.push({
|
|
78
|
+
key,
|
|
79
|
+
sessionId: typeof record.sessionId === "string" && record.sessionId.length > 0 ? record.sessionId : null,
|
|
80
|
+
agentId,
|
|
81
|
+
agentName,
|
|
82
|
+
displayName,
|
|
83
|
+
kind: typeof record.kind === "string" ? record.kind : null,
|
|
84
|
+
updatedAt: iso,
|
|
85
|
+
updatedAtMs: ms,
|
|
86
|
+
abortedLastRun: Boolean(record.abortedLastRun),
|
|
87
|
+
systemSent: Boolean(record.systemSent),
|
|
88
|
+
modelProvider: typeof record.modelProvider === "string" ? record.modelProvider : null,
|
|
89
|
+
model: typeof record.model === "string" ? record.model : null,
|
|
90
|
+
contextTokens: typeof record.contextTokens === "number" ? record.contextTokens : null,
|
|
91
|
+
totalTokens: typeof record.totalTokens === "number" ? record.totalTokens : null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return sessions.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
95
|
+
}
|
|
96
|
+
function buildAgentList(config, sessions) {
|
|
97
|
+
const nowMs = Date.now();
|
|
98
|
+
const configured = new Map();
|
|
99
|
+
for (const entry of config?.agents?.list ?? []) {
|
|
100
|
+
if (!entry || typeof entry !== "object")
|
|
101
|
+
continue;
|
|
102
|
+
if (typeof entry.id !== "string" || entry.id.trim().length === 0)
|
|
103
|
+
continue;
|
|
104
|
+
const id = entry.id.trim();
|
|
105
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0 ? entry.name.trim() : id;
|
|
106
|
+
configured.set(id, name);
|
|
107
|
+
}
|
|
108
|
+
const latestByAgent = new Map();
|
|
109
|
+
for (const session of sessions) {
|
|
110
|
+
if (!session.agentId)
|
|
111
|
+
continue;
|
|
112
|
+
const existing = latestByAgent.get(session.agentId);
|
|
113
|
+
if (!existing || session.updatedAtMs > existing.updatedAtMs) {
|
|
114
|
+
latestByAgent.set(session.agentId, session);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const agentIds = new Set([...configured.keys(), ...latestByAgent.keys()]);
|
|
118
|
+
const agents = [];
|
|
119
|
+
for (const id of agentIds) {
|
|
120
|
+
const latest = latestByAgent.get(id) ?? null;
|
|
121
|
+
agents.push({
|
|
122
|
+
id,
|
|
123
|
+
name: configured.get(id) ?? latest?.agentName ?? id,
|
|
124
|
+
status: determineAgentStatus(latest, nowMs),
|
|
125
|
+
currentTask: latest?.displayName ?? null,
|
|
126
|
+
runId: latest?.sessionId ?? latest?.key ?? null,
|
|
127
|
+
initiativeId: id ? `agent:${id}` : null,
|
|
128
|
+
startedAt: latest?.updatedAt ?? null,
|
|
129
|
+
blockers: latest?.abortedLastRun ? ["Last run was aborted"] : [],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
133
|
+
}
|
|
134
|
+
function withSessionLimit(snapshot, limit) {
|
|
135
|
+
if (snapshot.sessions.length <= limit) {
|
|
136
|
+
return snapshot;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...snapshot,
|
|
140
|
+
sessions: snapshot.sessions.slice(0, limit),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function readLocalOpenClawSnapshot(limit) {
|
|
144
|
+
const baseDir = join(homedir(), ".openclaw");
|
|
145
|
+
const configPath = join(baseDir, "openclaw.json");
|
|
146
|
+
const config = await readJsonFile(configPath);
|
|
147
|
+
const configuredAgents = new Map();
|
|
148
|
+
for (const entry of config?.agents?.list ?? []) {
|
|
149
|
+
if (typeof entry?.id !== "string")
|
|
150
|
+
continue;
|
|
151
|
+
const id = entry.id.trim();
|
|
152
|
+
if (!id)
|
|
153
|
+
continue;
|
|
154
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0 ? entry.name.trim() : id;
|
|
155
|
+
configuredAgents.set(id, name);
|
|
156
|
+
}
|
|
157
|
+
const defaultAgentId = resolveDefaultAgentId(config);
|
|
158
|
+
const sessionsPath = join(baseDir, "agents", defaultAgentId, "sessions", "sessions.json");
|
|
159
|
+
const sessionMap = await readJsonFile(sessionsPath);
|
|
160
|
+
const sessions = normalizeSessions(sessionMap, configuredAgents).slice(0, Math.max(1, limit));
|
|
161
|
+
const agents = buildAgentList(config, sessions);
|
|
162
|
+
return {
|
|
163
|
+
fetchedAt: new Date().toISOString(),
|
|
164
|
+
sessions,
|
|
165
|
+
agents,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export async function loadLocalOpenClawSnapshot(limit = 200) {
|
|
169
|
+
const normalizedLimit = Math.max(1, limit);
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (localSnapshotCache &&
|
|
172
|
+
now - localSnapshotCache.fetchedAtMs <= LOCAL_SNAPSHOT_CACHE_TTL_MS &&
|
|
173
|
+
localSnapshotCache.limit >= normalizedLimit) {
|
|
174
|
+
return withSessionLimit(localSnapshotCache.snapshot, normalizedLimit);
|
|
175
|
+
}
|
|
176
|
+
const snapshot = await readLocalOpenClawSnapshot(normalizedLimit);
|
|
177
|
+
localSnapshotCache = {
|
|
178
|
+
fetchedAtMs: now,
|
|
179
|
+
limit: normalizedLimit,
|
|
180
|
+
snapshot,
|
|
181
|
+
};
|
|
182
|
+
return snapshot;
|
|
183
|
+
}
|
|
184
|
+
function deriveSessionStatus(session) {
|
|
185
|
+
if (session.abortedLastRun)
|
|
186
|
+
return "failed";
|
|
187
|
+
const ageMs = Date.now() - session.updatedAtMs;
|
|
188
|
+
if (session.updatedAtMs > 0 && ageMs <= 5 * 60_000)
|
|
189
|
+
return "running";
|
|
190
|
+
if (session.updatedAtMs > 0 && ageMs <= ACTIVE_WINDOW_MS)
|
|
191
|
+
return "queued";
|
|
192
|
+
return "archived";
|
|
193
|
+
}
|
|
194
|
+
export function toLocalSessionTree(snapshot, limit = 100) {
|
|
195
|
+
const nodes = snapshot.sessions.slice(0, Math.max(1, limit)).map((session) => {
|
|
196
|
+
const groupId = session.agentId ? `agent:${session.agentId}` : "local";
|
|
197
|
+
const groupLabel = session.agentName ? `Agent ${session.agentName}` : "Local Sessions";
|
|
198
|
+
return {
|
|
199
|
+
id: session.sessionId ?? session.key,
|
|
200
|
+
parentId: null,
|
|
201
|
+
runId: session.sessionId ?? session.key,
|
|
202
|
+
title: session.displayName,
|
|
203
|
+
agentId: session.agentId,
|
|
204
|
+
agentName: session.agentName,
|
|
205
|
+
status: deriveSessionStatus(session),
|
|
206
|
+
progress: null,
|
|
207
|
+
initiativeId: groupId,
|
|
208
|
+
workstreamId: null,
|
|
209
|
+
groupId,
|
|
210
|
+
groupLabel,
|
|
211
|
+
startedAt: session.updatedAt,
|
|
212
|
+
updatedAt: session.updatedAt,
|
|
213
|
+
lastEventAt: session.updatedAt,
|
|
214
|
+
lastEventSummary: session.systemSent
|
|
215
|
+
? "Session sent an update"
|
|
216
|
+
: "Local OpenClaw session activity",
|
|
217
|
+
blockers: session.abortedLastRun ? ["Last run was aborted"] : [],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
const groupsMap = new Map();
|
|
221
|
+
for (const node of nodes) {
|
|
222
|
+
if (!groupsMap.has(node.groupId)) {
|
|
223
|
+
groupsMap.set(node.groupId, {
|
|
224
|
+
id: node.groupId,
|
|
225
|
+
label: node.groupLabel,
|
|
226
|
+
status: node.status,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
nodes,
|
|
232
|
+
edges: [],
|
|
233
|
+
groups: Array.from(groupsMap.values()),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const JSONL_RECENT_WINDOW_MS = 7 * 24 * 60 * 60_000; // 7 days
|
|
237
|
+
const MAX_TURNS_PER_SESSION = 30;
|
|
238
|
+
const MAX_TOTAL_TURNS = 120;
|
|
239
|
+
const MAX_RAW_EVENTS = 600; // Cap raw events read from JSONL
|
|
240
|
+
const MAX_RAW_EVENTS_DETAIL = 5_000;
|
|
241
|
+
const USER_PROMPT_MAX_CHARS = 1_200;
|
|
242
|
+
const ERROR_TEXT_MAX_CHARS = 2_000;
|
|
243
|
+
const ASSISTANT_TEXT_MAX_CHARS = 20_000;
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Text extraction helpers
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
function extractText(content, maxLen) {
|
|
248
|
+
const clip = (value) => {
|
|
249
|
+
if (maxLen <= 0 || value.length <= maxLen)
|
|
250
|
+
return value;
|
|
251
|
+
return value.slice(0, maxLen) + "\u2026";
|
|
252
|
+
};
|
|
253
|
+
if (!content)
|
|
254
|
+
return "";
|
|
255
|
+
if (typeof content === "string") {
|
|
256
|
+
return clip(content);
|
|
257
|
+
}
|
|
258
|
+
if (!Array.isArray(content))
|
|
259
|
+
return "";
|
|
260
|
+
const textBlocks = content
|
|
261
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
262
|
+
.map((block) => block.text.trim())
|
|
263
|
+
.filter((block) => block.length > 0);
|
|
264
|
+
if (textBlocks.length > 0) {
|
|
265
|
+
return clip(textBlocks.join("\n\n"));
|
|
266
|
+
}
|
|
267
|
+
for (const block of content) {
|
|
268
|
+
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
269
|
+
return `[tool: ${block.name}]`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const block of content) {
|
|
273
|
+
if (block.type === "tool_result") {
|
|
274
|
+
const inner = block.content;
|
|
275
|
+
if (typeof inner === "string") {
|
|
276
|
+
return clip(inner);
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(inner)) {
|
|
279
|
+
const innerText = inner
|
|
280
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
281
|
+
.map((b) => b.text.trim())
|
|
282
|
+
.filter((b) => b.length > 0);
|
|
283
|
+
if (innerText.length > 0) {
|
|
284
|
+
return clip(innerText.join("\n\n"));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return "[tool result]";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
function collectToolNames(content) {
|
|
293
|
+
if (!Array.isArray(content))
|
|
294
|
+
return [];
|
|
295
|
+
return content
|
|
296
|
+
.filter((b) => b.type === "tool_use" && typeof b.name === "string")
|
|
297
|
+
.map((b) => b.name);
|
|
298
|
+
}
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// JSONL reading
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
async function readSessionEvents(session, baseDir, agentId, maxEvents) {
|
|
303
|
+
if (!session.sessionId)
|
|
304
|
+
return [];
|
|
305
|
+
const jsonlPath = join(baseDir, "agents", agentId, "sessions", `${session.sessionId}.jsonl`);
|
|
306
|
+
try {
|
|
307
|
+
const info = await stat(jsonlPath);
|
|
308
|
+
if (info.size === 0)
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const raw = await readFile(jsonlPath, "utf8");
|
|
316
|
+
const lines = raw.split("\n");
|
|
317
|
+
// Read from end (most recent first), but only message events
|
|
318
|
+
const events = [];
|
|
319
|
+
for (let i = lines.length - 1; i >= 0 && events.length < maxEvents; i--) {
|
|
320
|
+
const line = lines[i].trim();
|
|
321
|
+
if (line.length === 0)
|
|
322
|
+
continue;
|
|
323
|
+
try {
|
|
324
|
+
const evt = JSON.parse(line);
|
|
325
|
+
if (evt.type === "message" && evt.message && evt.timestamp) {
|
|
326
|
+
events.push(evt);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// skip
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Reverse so they're in chronological order for grouping
|
|
334
|
+
events.reverse();
|
|
335
|
+
return events;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Turn grouping
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
function groupEventsIntoTurns(events, maxTurns, limits) {
|
|
345
|
+
const userPromptMaxChars = limits?.userPromptMaxChars ?? USER_PROMPT_MAX_CHARS;
|
|
346
|
+
const errorTextMaxChars = limits?.errorTextMaxChars ?? ERROR_TEXT_MAX_CHARS;
|
|
347
|
+
const assistantTextMaxChars = limits?.assistantTextMaxChars ?? ASSISTANT_TEXT_MAX_CHARS;
|
|
348
|
+
const turns = [];
|
|
349
|
+
let current = null;
|
|
350
|
+
function finalizeTurn() {
|
|
351
|
+
if (!current)
|
|
352
|
+
return;
|
|
353
|
+
turns.push({
|
|
354
|
+
id: current.id,
|
|
355
|
+
userPrompt: current.userPrompt,
|
|
356
|
+
toolNames: [...new Set(current.toolNames)], // dedupe
|
|
357
|
+
assistantResponse: current.assistantTexts.length > 0
|
|
358
|
+
? current.assistantTexts[current.assistantTexts.length - 1]
|
|
359
|
+
: null,
|
|
360
|
+
errorMessage: current.errorMessage,
|
|
361
|
+
model: current.model,
|
|
362
|
+
timestamp: current.timestamp,
|
|
363
|
+
endTimestamp: current.endTimestamp,
|
|
364
|
+
costTotal: current.costTotal,
|
|
365
|
+
eventCount: current.eventCount,
|
|
366
|
+
});
|
|
367
|
+
current = null;
|
|
368
|
+
}
|
|
369
|
+
for (const evt of events) {
|
|
370
|
+
if (turns.length >= maxTurns)
|
|
371
|
+
break;
|
|
372
|
+
const msg = evt.message;
|
|
373
|
+
if (!msg)
|
|
374
|
+
continue;
|
|
375
|
+
const role = msg.role;
|
|
376
|
+
const ts = evt.timestamp ?? "";
|
|
377
|
+
const cost = msg.usage?.cost?.total ?? 0;
|
|
378
|
+
// A user message always starts a new turn
|
|
379
|
+
if (role === "user") {
|
|
380
|
+
finalizeTurn();
|
|
381
|
+
current = {
|
|
382
|
+
id: evt.id ?? `turn-${turns.length}`,
|
|
383
|
+
userPrompt: extractText(msg.content, userPromptMaxChars),
|
|
384
|
+
toolNames: [],
|
|
385
|
+
assistantTexts: [],
|
|
386
|
+
errorMessage: null,
|
|
387
|
+
model: null,
|
|
388
|
+
timestamp: ts,
|
|
389
|
+
endTimestamp: ts,
|
|
390
|
+
costTotal: 0,
|
|
391
|
+
eventCount: 1,
|
|
392
|
+
};
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// Ensure we have a current turn (auto-start for autonomous assistant messages)
|
|
396
|
+
if (!current) {
|
|
397
|
+
current = {
|
|
398
|
+
id: evt.id ?? `turn-${turns.length}`,
|
|
399
|
+
userPrompt: null,
|
|
400
|
+
toolNames: [],
|
|
401
|
+
assistantTexts: [],
|
|
402
|
+
errorMessage: null,
|
|
403
|
+
model: null,
|
|
404
|
+
timestamp: ts,
|
|
405
|
+
endTimestamp: ts,
|
|
406
|
+
costTotal: 0,
|
|
407
|
+
eventCount: 0,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
current.endTimestamp = ts;
|
|
411
|
+
current.eventCount += 1;
|
|
412
|
+
current.costTotal += cost;
|
|
413
|
+
if (role === "assistant") {
|
|
414
|
+
current.model = msg.model ?? current.model;
|
|
415
|
+
// Collect tool names from this message
|
|
416
|
+
const tools = collectToolNames(msg.content);
|
|
417
|
+
current.toolNames.push(...tools);
|
|
418
|
+
// Check for error
|
|
419
|
+
if (msg.stopReason === "error" || msg.errorMessage) {
|
|
420
|
+
current.errorMessage = msg.errorMessage ?? extractText(msg.content, errorTextMaxChars);
|
|
421
|
+
}
|
|
422
|
+
// Collect text response
|
|
423
|
+
const text = extractText(msg.content, assistantTextMaxChars);
|
|
424
|
+
if (text && !text.startsWith("[tool:")) {
|
|
425
|
+
current.assistantTexts.push(text);
|
|
426
|
+
}
|
|
427
|
+
// A completed assistant response (stop/end_turn) finalizes the turn
|
|
428
|
+
if (msg.stopReason === "stop" || msg.stopReason === "end_turn") {
|
|
429
|
+
finalizeTurn();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Tool results just accumulate into the current turn
|
|
433
|
+
if (role === "toolResult" || role === "tool") {
|
|
434
|
+
// Nothing special — they're part of the current turn
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Finalize any in-progress turn
|
|
438
|
+
finalizeTurn();
|
|
439
|
+
return turns;
|
|
440
|
+
}
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Rule-based turn summarization (fallback — LLM digest in Phase 1B)
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
function summarizeTurn(turn, _agentLabel) {
|
|
445
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
446
|
+
if (turn.errorMessage) {
|
|
447
|
+
return `Error: ${normalize(turn.errorMessage)}`;
|
|
448
|
+
}
|
|
449
|
+
// If there are tool calls, describe what was done
|
|
450
|
+
if (turn.toolNames.length > 0) {
|
|
451
|
+
const uniqueTools = [...new Set(turn.toolNames)];
|
|
452
|
+
const toolStr = uniqueTools.length <= 3
|
|
453
|
+
? uniqueTools.join(", ")
|
|
454
|
+
: `${uniqueTools.slice(0, 2).join(", ")} +${uniqueTools.length - 2} more`;
|
|
455
|
+
if (turn.assistantResponse) {
|
|
456
|
+
// Keep full text so detail modals can show complete context.
|
|
457
|
+
return normalize(turn.assistantResponse);
|
|
458
|
+
}
|
|
459
|
+
return `Used ${toolStr}`;
|
|
460
|
+
}
|
|
461
|
+
// Text-only response
|
|
462
|
+
if (turn.assistantResponse) {
|
|
463
|
+
return normalize(turn.assistantResponse);
|
|
464
|
+
}
|
|
465
|
+
if (turn.userPrompt) {
|
|
466
|
+
return `User: ${normalize(turn.userPrompt)}`;
|
|
467
|
+
}
|
|
468
|
+
return "Activity";
|
|
469
|
+
}
|
|
470
|
+
function isDigestSummaryStale(cached, fresh) {
|
|
471
|
+
if (!cached)
|
|
472
|
+
return true;
|
|
473
|
+
const trimmed = cached.trim();
|
|
474
|
+
if (!trimmed)
|
|
475
|
+
return true;
|
|
476
|
+
if (trimmed.endsWith("…") && fresh.length > trimmed.length)
|
|
477
|
+
return true;
|
|
478
|
+
const deEllipsized = trimmed.replace(/…$/, "");
|
|
479
|
+
if (deEllipsized.length > 0 &&
|
|
480
|
+
fresh.length > deEllipsized.length + 24 &&
|
|
481
|
+
fresh.startsWith(deEllipsized)) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
if (trimmed.startsWith("[tool:") && !fresh.startsWith("[tool:"))
|
|
485
|
+
return true;
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Digest cache — stores summarized turn titles alongside JSONL
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
async function readDigestCache(baseDir, agentId, sessionId) {
|
|
492
|
+
const cachePath = join(baseDir, "agents", agentId, "sessions", `${sessionId}.digest.json`);
|
|
493
|
+
try {
|
|
494
|
+
const raw = await readFile(cachePath, "utf8");
|
|
495
|
+
return JSON.parse(raw);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function writeDigestCache(baseDir, agentId, sessionId, cache) {
|
|
502
|
+
const cachePath = join(baseDir, "agents", agentId, "sessions", `${sessionId}.digest.json`);
|
|
503
|
+
try {
|
|
504
|
+
await writeFile(cachePath, JSON.stringify(cache), "utf8");
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Non-critical — cache miss next time is fine
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Turn → LiveActivityItem mapping
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
function turnToActivity(turn, session, cachedSummary, index) {
|
|
514
|
+
const agentLabel = session.agentName ?? session.agentId ?? "OpenClaw";
|
|
515
|
+
const summary = cachedSummary ?? summarizeTurn(turn, agentLabel);
|
|
516
|
+
// Determine activity type
|
|
517
|
+
let type = "delegation";
|
|
518
|
+
if (turn.errorMessage) {
|
|
519
|
+
type = "run_failed";
|
|
520
|
+
}
|
|
521
|
+
else if (turn.toolNames.length > 0) {
|
|
522
|
+
type = "artifact_created";
|
|
523
|
+
}
|
|
524
|
+
// Build title
|
|
525
|
+
let title;
|
|
526
|
+
if (turn.userPrompt && turn.toolNames.length === 0 && !turn.assistantResponse) {
|
|
527
|
+
// Standalone user prompt
|
|
528
|
+
title = summary;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
title = summary;
|
|
532
|
+
}
|
|
533
|
+
const modelAlias = humanizeModelShort(turn.model ?? session.model ?? null);
|
|
534
|
+
return {
|
|
535
|
+
id: `local:turn:${turn.id}:${index}`,
|
|
536
|
+
type,
|
|
537
|
+
title,
|
|
538
|
+
description: modelAlias,
|
|
539
|
+
agentId: session.agentId,
|
|
540
|
+
agentName: session.agentName,
|
|
541
|
+
runId: session.sessionId ?? session.key,
|
|
542
|
+
initiativeId: session.agentId ? `agent:${session.agentId}` : null,
|
|
543
|
+
timestamp: turn.timestamp,
|
|
544
|
+
summary: turn.assistantResponse
|
|
545
|
+
? turn.assistantResponse
|
|
546
|
+
: null,
|
|
547
|
+
metadata: {
|
|
548
|
+
source: "local_openclaw",
|
|
549
|
+
sessionKey: session.key,
|
|
550
|
+
turnId: turn.id,
|
|
551
|
+
toolNames: turn.toolNames.length > 0 ? turn.toolNames : undefined,
|
|
552
|
+
eventCount: turn.eventCount,
|
|
553
|
+
costTotal: turn.costTotal > 0 ? Math.round(turn.costTotal * 10000) / 10000 : undefined,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** Quick model alias for descriptions */
|
|
558
|
+
function humanizeModelShort(model) {
|
|
559
|
+
if (!model)
|
|
560
|
+
return null;
|
|
561
|
+
const lower = model.toLowerCase();
|
|
562
|
+
if (lower.includes("opus"))
|
|
563
|
+
return "Opus";
|
|
564
|
+
if (lower.includes("sonnet"))
|
|
565
|
+
return "Sonnet";
|
|
566
|
+
if (lower.includes("haiku"))
|
|
567
|
+
return "Haiku";
|
|
568
|
+
if (lower.includes("kimi"))
|
|
569
|
+
return "Kimi";
|
|
570
|
+
if (lower.includes("gemini"))
|
|
571
|
+
return "Gemini";
|
|
572
|
+
if (lower.includes("gpt-4"))
|
|
573
|
+
return "GPT-4";
|
|
574
|
+
if (lower.includes("qwen"))
|
|
575
|
+
return "Qwen";
|
|
576
|
+
// Strip provider prefix for others
|
|
577
|
+
const parts = model.split("/");
|
|
578
|
+
return parts[parts.length - 1] ?? model;
|
|
579
|
+
}
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// Main activity builder (turn-based)
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
export async function toLocalLiveActivity(snapshot, limit = 200) {
|
|
584
|
+
const baseDir = join(homedir(), ".openclaw");
|
|
585
|
+
const nowMs = Date.now();
|
|
586
|
+
const recentCutoff = nowMs - JSONL_RECENT_WINDOW_MS;
|
|
587
|
+
const totalCap = Math.min(limit, MAX_TOTAL_TURNS);
|
|
588
|
+
const allActivities = [];
|
|
589
|
+
const defaultAgentId = snapshot.sessions.find((s) => s.agentId)?.agentId ?? "main";
|
|
590
|
+
for (const session of snapshot.sessions) {
|
|
591
|
+
if (allActivities.length >= totalCap)
|
|
592
|
+
break;
|
|
593
|
+
const hasSessionFile = Boolean(session.sessionId);
|
|
594
|
+
const isRecent = session.updatedAtMs >= recentCutoff;
|
|
595
|
+
if (hasSessionFile && isRecent) {
|
|
596
|
+
const agentId = session.agentId ?? defaultAgentId;
|
|
597
|
+
const remaining = totalCap - allActivities.length;
|
|
598
|
+
const perSessionCap = Math.min(MAX_TURNS_PER_SESSION, remaining);
|
|
599
|
+
// Read events and group into turns
|
|
600
|
+
const events = await readSessionEvents(session, baseDir, agentId, MAX_RAW_EVENTS);
|
|
601
|
+
const turns = groupEventsIntoTurns(events, perSessionCap);
|
|
602
|
+
if (turns.length === 0) {
|
|
603
|
+
allActivities.push(makeSessionSummaryItem(session));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
// Check digest cache for pre-computed summaries
|
|
607
|
+
let cache = await readDigestCache(baseDir, agentId, session.sessionId);
|
|
608
|
+
const cachedMap = new Map();
|
|
609
|
+
if (cache) {
|
|
610
|
+
for (const entry of cache.entries) {
|
|
611
|
+
cachedMap.set(entry.turnId, entry.summary);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Build activity items from turns (most recent first)
|
|
615
|
+
const newCacheEntries = [];
|
|
616
|
+
for (let i = turns.length - 1; i >= 0 && allActivities.length < totalCap; i--) {
|
|
617
|
+
const turn = turns[i];
|
|
618
|
+
const cached = cachedMap.get(turn.id) ?? null;
|
|
619
|
+
const agentLabel = session.agentName ?? session.agentId ?? "OpenClaw";
|
|
620
|
+
const computedSummary = summarizeTurn(turn, agentLabel);
|
|
621
|
+
const fallbackSummary = isDigestSummaryStale(cached, computedSummary)
|
|
622
|
+
? computedSummary
|
|
623
|
+
: cached;
|
|
624
|
+
allActivities.push(turnToActivity(turn, session, fallbackSummary, i));
|
|
625
|
+
// Track for cache
|
|
626
|
+
if (isDigestSummaryStale(cached, computedSummary)) {
|
|
627
|
+
newCacheEntries.push({ turnId: turn.id, summary: fallbackSummary });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Update digest cache if we have new entries
|
|
631
|
+
if (newCacheEntries.length > 0) {
|
|
632
|
+
const byTurn = new Map();
|
|
633
|
+
for (const entry of cache?.entries ?? []) {
|
|
634
|
+
byTurn.set(entry.turnId, entry);
|
|
635
|
+
}
|
|
636
|
+
for (const entry of newCacheEntries) {
|
|
637
|
+
byTurn.set(entry.turnId, entry);
|
|
638
|
+
}
|
|
639
|
+
const updatedEntries = Array.from(byTurn.values());
|
|
640
|
+
await writeDigestCache(baseDir, agentId, session.sessionId, {
|
|
641
|
+
sessionId: session.sessionId,
|
|
642
|
+
updatedAtMs: nowMs,
|
|
643
|
+
entries: updatedEntries,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
allActivities.push(makeSessionSummaryItem(session));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
activities: allActivities,
|
|
653
|
+
total: allActivities.length,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
export async function loadLocalTurnDetail(input) {
|
|
657
|
+
const turnId = input.turnId.trim();
|
|
658
|
+
if (!turnId)
|
|
659
|
+
return null;
|
|
660
|
+
const sessionKey = input.sessionKey?.trim() || null;
|
|
661
|
+
const runId = input.runId?.trim() || null;
|
|
662
|
+
const snapshot = await loadLocalOpenClawSnapshot(400);
|
|
663
|
+
const session = snapshot.sessions.find((candidate) => {
|
|
664
|
+
if (sessionKey && candidate.key === sessionKey)
|
|
665
|
+
return true;
|
|
666
|
+
if (!runId)
|
|
667
|
+
return false;
|
|
668
|
+
return candidate.sessionId === runId || candidate.key === runId;
|
|
669
|
+
});
|
|
670
|
+
if (!session || !session.sessionId)
|
|
671
|
+
return null;
|
|
672
|
+
const baseDir = join(homedir(), ".openclaw");
|
|
673
|
+
const defaultAgentId = snapshot.sessions.find((s) => s.agentId)?.agentId ?? "main";
|
|
674
|
+
const agentId = session.agentId ?? defaultAgentId;
|
|
675
|
+
const events = await readSessionEvents(session, baseDir, agentId, MAX_RAW_EVENTS_DETAIL);
|
|
676
|
+
const turns = groupEventsIntoTurns(events, Math.max(MAX_TURNS_PER_SESSION * 10, 400), {
|
|
677
|
+
assistantTextMaxChars: 0,
|
|
678
|
+
});
|
|
679
|
+
const turn = turns.find((candidate) => candidate.id === turnId);
|
|
680
|
+
if (!turn)
|
|
681
|
+
return null;
|
|
682
|
+
return {
|
|
683
|
+
turnId: turn.id,
|
|
684
|
+
summary: turn.assistantResponse,
|
|
685
|
+
userPrompt: turn.userPrompt,
|
|
686
|
+
timestamp: turn.timestamp ?? null,
|
|
687
|
+
model: turn.model,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function makeSessionSummaryItem(session) {
|
|
691
|
+
const type = session.abortedLastRun
|
|
692
|
+
? "run_failed"
|
|
693
|
+
: deriveSessionStatus(session) === "running"
|
|
694
|
+
? "run_started"
|
|
695
|
+
: "delegation";
|
|
696
|
+
const modelAlias = humanizeModelShort([session.modelProvider, session.model].filter(Boolean).join("/") || null);
|
|
697
|
+
return {
|
|
698
|
+
id: `local:${session.key}:${session.updatedAtMs}`,
|
|
699
|
+
type,
|
|
700
|
+
title: type === "run_failed"
|
|
701
|
+
? `Session failed: ${session.displayName}`
|
|
702
|
+
: type === "run_started"
|
|
703
|
+
? `Session active: ${session.displayName}`
|
|
704
|
+
: `Session update: ${session.displayName}`,
|
|
705
|
+
description: modelAlias ? `Local session (${modelAlias})` : "Local session",
|
|
706
|
+
agentId: session.agentId,
|
|
707
|
+
agentName: session.agentName,
|
|
708
|
+
runId: session.sessionId ?? session.key,
|
|
709
|
+
initiativeId: session.agentId ? `agent:${session.agentId}` : null,
|
|
710
|
+
timestamp: session.updatedAt ?? new Date().toISOString(),
|
|
711
|
+
metadata: {
|
|
712
|
+
source: "local_openclaw",
|
|
713
|
+
sessionKey: session.key,
|
|
714
|
+
kind: session.kind,
|
|
715
|
+
totalTokens: session.totalTokens,
|
|
716
|
+
contextTokens: session.contextTokens,
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
export function toLocalLiveAgents(snapshot) {
|
|
721
|
+
const summary = {};
|
|
722
|
+
const agents = snapshot.agents.map((agent) => {
|
|
723
|
+
summary[agent.status] = (summary[agent.status] ?? 0) + 1;
|
|
724
|
+
return {
|
|
725
|
+
id: agent.id,
|
|
726
|
+
name: agent.name,
|
|
727
|
+
status: agent.status,
|
|
728
|
+
currentTask: agent.currentTask,
|
|
729
|
+
runId: agent.runId,
|
|
730
|
+
initiativeId: agent.initiativeId,
|
|
731
|
+
startedAt: agent.startedAt,
|
|
732
|
+
blockers: agent.blockers,
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
return { agents, summary };
|
|
736
|
+
}
|
|
737
|
+
export function toLocalLiveInitiatives(snapshot) {
|
|
738
|
+
const byAgent = new Map();
|
|
739
|
+
for (const session of snapshot.sessions) {
|
|
740
|
+
const groupId = session.agentId ? `agent:${session.agentId}` : "agent:unknown";
|
|
741
|
+
const existing = byAgent.get(groupId) ?? {
|
|
742
|
+
id: groupId,
|
|
743
|
+
title: session.agentName ? `Agent ${session.agentName}` : "Unassigned",
|
|
744
|
+
status: "active",
|
|
745
|
+
updatedAt: session.updatedAt,
|
|
746
|
+
updatedAtMs: session.updatedAtMs,
|
|
747
|
+
sessionCount: 0,
|
|
748
|
+
activeAgents: new Set(),
|
|
749
|
+
};
|
|
750
|
+
existing.sessionCount += 1;
|
|
751
|
+
if (session.agentId)
|
|
752
|
+
existing.activeAgents.add(session.agentId);
|
|
753
|
+
if (session.updatedAtMs > existing.updatedAtMs) {
|
|
754
|
+
existing.updatedAtMs = session.updatedAtMs;
|
|
755
|
+
existing.updatedAt = session.updatedAt;
|
|
756
|
+
}
|
|
757
|
+
byAgent.set(groupId, existing);
|
|
758
|
+
}
|
|
759
|
+
const initiatives = Array.from(byAgent.values())
|
|
760
|
+
.sort((a, b) => b.updatedAtMs - a.updatedAtMs)
|
|
761
|
+
.map((entry) => ({
|
|
762
|
+
id: entry.id,
|
|
763
|
+
title: entry.title,
|
|
764
|
+
status: entry.status,
|
|
765
|
+
updatedAt: entry.updatedAt,
|
|
766
|
+
sessionCount: entry.sessionCount,
|
|
767
|
+
activeAgents: entry.activeAgents.size,
|
|
768
|
+
}));
|
|
769
|
+
return {
|
|
770
|
+
initiatives,
|
|
771
|
+
total: initiatives.length,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
//# sourceMappingURL=local-openclaw.js.map
|