-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathsessionLogParser.ts
More file actions
163 lines (138 loc) · 6.02 KB
/
Copy pathsessionLogParser.ts
File metadata and controls
163 lines (138 loc) · 6.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @file sessionLogParser.ts
* @description Pure functions for parsing OpenClaw session JSONL files and
* detecting task completion / inactivity signals. Extracted from
* sessionHistoryPoller.ts so they can be tested independently of the
* stateful polling timers.
*/
import path from 'path';
import { getTaskSubagentSession } from './subagentSessions.js';
import { logger } from './logger.js';
// ── Shared function-type aliases ─────────────────────────────────────────────
/** Callback type used by the poller to forward events to the SaaS backend. */
export type SendToSaasFn = (payload: any) => void;
/** Callback type used to mark an agent as running in the websocket layer. */
export type SetAgentRunningFn = (agentId: string) => void;
// ── Result types ─────────────────────────────────────────────────────────────
/** Returned by {@link checkSessionInactivity}. */
export interface InactivityCheckResult {
shouldWrite: boolean;
outputPath?: string;
}
// ── Pure parsing helpers ──────────────────────────────────────────────────────
/**
* Scans a list of `{role, text}` messages for the first
* `TASK_COMPLETED` / `TASK_FAILED` / `TASK_BLOCKED` status tag and returns the
* canonical SaaS status string, or `null` if none found.
*
* When `expectedTaskId` is supplied, tags carrying a different task UUID are
* silently skipped to avoid mis-completion from stale context.
*/
export function detectStatusTagFromMessages(
messages: Array<{ role: string; text: string }>,
expectedTaskId?: string
): 'done' | 'failed' | 'blocked' | null {
const statusTagRe = /<(TASK_COMPLETED|TASK_FAILED|TASK_BLOCKED):([a-f0-9\-]+)>/i;
for (const m of messages) {
const match = statusTagRe.exec(m.text);
if (!match) continue;
const tag = match[1].toUpperCase();
const tagTaskId = match[2].toLowerCase();
if (expectedTaskId && tagTaskId !== expectedTaskId.toLowerCase()) continue;
return tag === 'TASK_COMPLETED' ? 'done'
: tag === 'TASK_FAILED' ? 'failed'
: 'blocked';
}
return null;
}
/**
* Resolves the OpenClaw session entry for a given `agentId` / `taskId` from
* the parsed `sessions.json` index object. Resolution priority:
*
* 0. Deterministic dispatch mapping (taskId → session key via subagentSessions).
* 1. Exact `agent:{agentId}:subagent:{taskId}` key.
* 2. Main session (`agent:{agentId}:main` or legacy `main`).
*/
export function resolveTaskSessionEntry(
index: Record<string, any>,
agentId: string,
taskId?: string
): any | null {
const subagentPrefix = `agent:${agentId}:subagent:`;
const mainKey = `agent:${agentId}:main`;
// 0) Deterministic mapping (taskId → childSessionKey) registered at dispatch.
if (taskId) {
try {
const mappedKey = getTaskSubagentSession(taskId);
if (mappedKey) {
const fullMappedKey = mappedKey.startsWith(`agent:${agentId}:`)
? mappedKey
: `${subagentPrefix}${mappedKey}`;
if (index[fullMappedKey]) return index[fullMappedKey];
if (index[mappedKey]) return index[mappedKey];
}
} catch {
/* ignore — mapping not found, fall through */
}
}
// 1) Exact subagent key for this task UUID.
if (taskId && index[`${subagentPrefix}${taskId}`]) return index[`${subagentPrefix}${taskId}`];
// 2) Main session.
if (index[mainKey]) return index[mainKey];
if (index['main']) return index['main'];
logger.warn('poller.session_resolution_failed', { agentId, taskId, availableKeys: Object.keys(index) });
return null;
}
/**
* Parses raw JSONL lines from an OpenClaw session file into structured
* `{role, text}` message objects (text capped at 5 000 chars per message).
* Lines that are not `type: "message"` entries or that fail to parse are
* silently dropped.
*/
export function parseMessagesFromLines(lines: string[]): Array<{ role: string; text: string }> {
return lines.map(line => {
try {
const e = JSON.parse(line);
if (e.type !== 'message' || !e.message) return null;
const role: string = e.message.role;
const content = e.message.content;
const text = Array.isArray(content)
? content.filter((c: any) => c.type === 'text').map((c: any) => String(c.text)).join('').slice(0, 5000)
: (typeof content === 'string' ? content.slice(0, 5000) : '');
return text ? { role, text } : null;
} catch {
return null;
}
}).filter(Boolean) as Array<{ role: string; text: string }>;
}
/**
* Determines whether the most recent activity in a session's JSONL lines
* exceeds `thresholdMinutes` of inactivity. When it does, returns the
* canonical `agent_log.md` output path so the caller can write or upload it.
*/
export function checkSessionInactivity(
lines: string[],
taskFolderName: string | undefined,
workspaceDir: string | undefined,
thresholdMinutes: number
): InactivityCheckResult {
if (!taskFolderName || lines.length === 0) return { shouldWrite: false };
// Find the last entry with a top-level `timestamp` field.
let lastTimestampMs: number | null = null;
for (let i = lines.length - 1; i >= 0; i--) {
try {
const e = JSON.parse(lines[i]);
if (e.timestamp) {
lastTimestampMs = Date.parse(e.timestamp);
break;
}
} catch { /* ignore malformed lines */ }
}
if (lastTimestampMs === null || isNaN(lastTimestampMs)) return { shouldWrite: false };
const elapsedMs = Date.now() - lastTimestampMs;
if (elapsedMs <= thresholdMinutes * 60_000) return { shouldWrite: false };
const outputPath = path.isAbsolute(taskFolderName)
? path.join(taskFolderName, 'output', 'agent_log.md')
: path.join(workspaceDir ?? '', taskFolderName, 'output', 'agent_log.md');
return { shouldWrite: true, outputPath };
}