@useorgx/openclaw-plugin 0.2.1 → 0.3.1

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