@steadwing/openalerts 0.2.4 → 0.2.5
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 +6 -2
- package/dist/collections/collection-manager.d.ts +50 -0
- package/dist/collections/collection-manager.js +583 -0
- package/dist/collections/event-parser.d.ts +27 -0
- package/dist/collections/event-parser.js +321 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.js +6 -0
- package/dist/collections/persistence.d.ts +25 -0
- package/dist/collections/persistence.js +213 -0
- package/dist/collections/types.d.ts +177 -0
- package/dist/collections/types.js +15 -0
- package/dist/core/engine.js +2 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/rules.js +97 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/index.js +410 -23
- package/dist/plugin/dashboard-html.js +33 -3
- package/dist/plugin/dashboard-routes.d.ts +7 -2
- package/dist/plugin/dashboard-routes.js +111 -3
- package/dist/plugin/gateway-client.js +15 -8
- package/openclaw.plugin.json +30 -0
- package/package.json +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
export function sessionInfoToMonitor(info) {
|
|
2
|
+
return {
|
|
3
|
+
key: info.key,
|
|
4
|
+
agentId: info.agentId,
|
|
5
|
+
lastActivityAt: info.lastActivityAt,
|
|
6
|
+
status: "idle",
|
|
7
|
+
spawnedBy: info.spawnedBy,
|
|
8
|
+
messageCount: info.messageCount,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// ─── Chat Event Parser ───────────────────────────────────────────────────────
|
|
12
|
+
export function chatEventToAction(event) {
|
|
13
|
+
let type = "streaming";
|
|
14
|
+
if (event.state === "final")
|
|
15
|
+
type = "complete";
|
|
16
|
+
else if (event.state === "delta")
|
|
17
|
+
type = "streaming";
|
|
18
|
+
else if (event.state === "aborted")
|
|
19
|
+
type = "aborted";
|
|
20
|
+
else if (event.state === "error")
|
|
21
|
+
type = "error";
|
|
22
|
+
const action = {
|
|
23
|
+
id: `${event.runId}-${event.seq}`,
|
|
24
|
+
runId: event.runId,
|
|
25
|
+
sessionKey: event.sessionKey,
|
|
26
|
+
seq: event.seq,
|
|
27
|
+
type,
|
|
28
|
+
eventType: "chat",
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
if (event.state === "final") {
|
|
32
|
+
if (event.usage) {
|
|
33
|
+
action.inputTokens = event.usage.inputTokens;
|
|
34
|
+
action.outputTokens = event.usage.outputTokens;
|
|
35
|
+
}
|
|
36
|
+
if (event.stopReason) {
|
|
37
|
+
action.stopReason = event.stopReason;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (event.message) {
|
|
41
|
+
if (typeof event.message === "string") {
|
|
42
|
+
action.content = event.message;
|
|
43
|
+
}
|
|
44
|
+
else if (typeof event.message === "object") {
|
|
45
|
+
const msg = event.message;
|
|
46
|
+
if (Array.isArray(msg.content)) {
|
|
47
|
+
const texts = [];
|
|
48
|
+
for (const block of msg.content) {
|
|
49
|
+
if (typeof block === "object" && block) {
|
|
50
|
+
const b = block;
|
|
51
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
52
|
+
texts.push(b.text);
|
|
53
|
+
}
|
|
54
|
+
else if (b.type === "tool_use") {
|
|
55
|
+
action.type = "tool_call";
|
|
56
|
+
action.toolName = String(b.name || "unknown");
|
|
57
|
+
action.toolArgs = b.input;
|
|
58
|
+
}
|
|
59
|
+
else if (b.type === "tool_result") {
|
|
60
|
+
action.type = "tool_result";
|
|
61
|
+
if (typeof b.content === "string") {
|
|
62
|
+
texts.push(b.content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (texts.length > 0) {
|
|
68
|
+
action.content = texts.join("");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (typeof msg.content === "string") {
|
|
72
|
+
action.content = msg.content;
|
|
73
|
+
}
|
|
74
|
+
else if (typeof msg.text === "string") {
|
|
75
|
+
action.content = msg.text;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (event.errorMessage) {
|
|
80
|
+
action.content = event.errorMessage;
|
|
81
|
+
}
|
|
82
|
+
return action;
|
|
83
|
+
}
|
|
84
|
+
// ─── Agent Event Parser ──────────────────────────────────────────────────────
|
|
85
|
+
export function agentEventToAction(event) {
|
|
86
|
+
const data = event.data;
|
|
87
|
+
let type = "streaming";
|
|
88
|
+
let content;
|
|
89
|
+
let toolName;
|
|
90
|
+
let toolArgs;
|
|
91
|
+
let startedAt;
|
|
92
|
+
let endedAt;
|
|
93
|
+
if (event.stream === "lifecycle") {
|
|
94
|
+
if (data.phase === "start") {
|
|
95
|
+
type = "start";
|
|
96
|
+
startedAt = typeof data.startedAt === "number" ? data.startedAt : event.ts;
|
|
97
|
+
}
|
|
98
|
+
else if (data.phase === "end") {
|
|
99
|
+
type = "complete";
|
|
100
|
+
endedAt = typeof data.endedAt === "number" ? data.endedAt : event.ts;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (data.type === "tool_use") {
|
|
104
|
+
type = "tool_call";
|
|
105
|
+
toolName = String(data.name || "unknown");
|
|
106
|
+
toolArgs = data.input;
|
|
107
|
+
content = `Tool: ${toolName}`;
|
|
108
|
+
}
|
|
109
|
+
else if (data.type === "tool_result") {
|
|
110
|
+
type = "tool_result";
|
|
111
|
+
content = String(data.content || "");
|
|
112
|
+
}
|
|
113
|
+
else if (data.type === "text" || typeof data.text === "string") {
|
|
114
|
+
type = "streaming";
|
|
115
|
+
content = String(data.text || "");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: `${event.runId}-${event.seq}`,
|
|
119
|
+
runId: event.runId,
|
|
120
|
+
sessionKey: event.sessionKey || event.stream,
|
|
121
|
+
seq: event.seq,
|
|
122
|
+
type,
|
|
123
|
+
eventType: "agent",
|
|
124
|
+
timestamp: event.ts,
|
|
125
|
+
content,
|
|
126
|
+
toolName,
|
|
127
|
+
toolArgs,
|
|
128
|
+
startedAt,
|
|
129
|
+
endedAt,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// ─── Exec Event Parser ───────────────────────────────────────────────────────
|
|
133
|
+
export function execStartedToEvent(event, seq) {
|
|
134
|
+
const execId = `exec-${event.runId}-${event.pid}`;
|
|
135
|
+
const timestamp = Date.now();
|
|
136
|
+
const id = seq != null ? `${execId}-started-${seq}` : `${execId}-started-${timestamp}`;
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
execId,
|
|
140
|
+
runId: event.runId,
|
|
141
|
+
pid: event.pid,
|
|
142
|
+
sessionId: event.sessionId,
|
|
143
|
+
eventType: "started",
|
|
144
|
+
command: event.command,
|
|
145
|
+
startedAt: event.startedAt,
|
|
146
|
+
timestamp,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function execOutputToEvent(event, seq) {
|
|
150
|
+
const execId = `exec-${event.runId}-${event.pid}`;
|
|
151
|
+
const timestamp = Date.now();
|
|
152
|
+
const id = seq != null ? `${execId}-output-${seq}` : `${execId}-output-${timestamp}`;
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
execId,
|
|
156
|
+
runId: event.runId,
|
|
157
|
+
pid: event.pid,
|
|
158
|
+
sessionId: event.sessionId,
|
|
159
|
+
eventType: "output",
|
|
160
|
+
stream: event.stream,
|
|
161
|
+
output: event.output,
|
|
162
|
+
timestamp,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function execCompletedToEvent(event, seq) {
|
|
166
|
+
const execId = `exec-${event.runId}-${event.pid}`;
|
|
167
|
+
const timestamp = Date.now();
|
|
168
|
+
const id = seq != null
|
|
169
|
+
? `${execId}-completed-${seq}`
|
|
170
|
+
: `${execId}-completed-${timestamp}`;
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
execId,
|
|
174
|
+
runId: event.runId,
|
|
175
|
+
pid: event.pid,
|
|
176
|
+
sessionId: event.sessionId,
|
|
177
|
+
eventType: "completed",
|
|
178
|
+
durationMs: event.durationMs,
|
|
179
|
+
exitCode: event.exitCode,
|
|
180
|
+
status: event.status,
|
|
181
|
+
timestamp,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function parseGatewayEvent(eventName, payload, seq) {
|
|
185
|
+
if (eventName === "health" || eventName === "tick") {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (eventName === "chat" && payload) {
|
|
189
|
+
const chatEvent = payload;
|
|
190
|
+
return {
|
|
191
|
+
action: chatEventToAction(chatEvent),
|
|
192
|
+
session: {
|
|
193
|
+
key: chatEvent.sessionKey,
|
|
194
|
+
status: chatEvent.state === "delta" ? "thinking" : "active",
|
|
195
|
+
lastActivityAt: Date.now(),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (eventName === "agent" && payload) {
|
|
200
|
+
const agentEvent = payload;
|
|
201
|
+
if (agentEvent.stream === "lifecycle") {
|
|
202
|
+
return {
|
|
203
|
+
action: agentEventToAction(agentEvent),
|
|
204
|
+
session: agentEvent.sessionKey
|
|
205
|
+
? {
|
|
206
|
+
key: agentEvent.sessionKey,
|
|
207
|
+
status: agentEvent.data?.phase === "start" ? "thinking" : "active",
|
|
208
|
+
lastActivityAt: Date.now(),
|
|
209
|
+
}
|
|
210
|
+
: undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (agentEvent.stream === "assistant" &&
|
|
214
|
+
typeof agentEvent.data?.text === "string") {
|
|
215
|
+
return {
|
|
216
|
+
action: agentEventToAction(agentEvent),
|
|
217
|
+
session: agentEvent.sessionKey
|
|
218
|
+
? {
|
|
219
|
+
key: agentEvent.sessionKey,
|
|
220
|
+
status: "thinking",
|
|
221
|
+
lastActivityAt: Date.now(),
|
|
222
|
+
}
|
|
223
|
+
: undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (agentEvent.data?.type === "tool_use" ||
|
|
227
|
+
agentEvent.data?.type === "tool_result") {
|
|
228
|
+
return {
|
|
229
|
+
action: agentEventToAction(agentEvent),
|
|
230
|
+
session: agentEvent.sessionKey
|
|
231
|
+
? {
|
|
232
|
+
key: agentEvent.sessionKey,
|
|
233
|
+
status: "thinking",
|
|
234
|
+
lastActivityAt: Date.now(),
|
|
235
|
+
}
|
|
236
|
+
: undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (eventName === "exec.started" && payload) {
|
|
242
|
+
const exec = payload;
|
|
243
|
+
const execEvent = execStartedToEvent(exec, seq);
|
|
244
|
+
return {
|
|
245
|
+
execEvent,
|
|
246
|
+
session: exec.sessionId
|
|
247
|
+
? {
|
|
248
|
+
key: exec.sessionId,
|
|
249
|
+
status: "thinking",
|
|
250
|
+
lastActivityAt: execEvent.timestamp,
|
|
251
|
+
}
|
|
252
|
+
: undefined,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (eventName === "exec.output" && payload) {
|
|
256
|
+
const exec = payload;
|
|
257
|
+
const execEvent = execOutputToEvent(exec, seq);
|
|
258
|
+
return {
|
|
259
|
+
execEvent,
|
|
260
|
+
session: exec.sessionId
|
|
261
|
+
? {
|
|
262
|
+
key: exec.sessionId,
|
|
263
|
+
lastActivityAt: execEvent.timestamp,
|
|
264
|
+
}
|
|
265
|
+
: undefined,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (eventName === "exec.completed" && payload) {
|
|
269
|
+
const exec = payload;
|
|
270
|
+
const execEvent = execCompletedToEvent(exec, seq);
|
|
271
|
+
return {
|
|
272
|
+
execEvent,
|
|
273
|
+
session: exec.sessionId
|
|
274
|
+
? {
|
|
275
|
+
key: exec.sessionId,
|
|
276
|
+
status: "active",
|
|
277
|
+
lastActivityAt: execEvent.timestamp,
|
|
278
|
+
}
|
|
279
|
+
: undefined,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
export function diagnosticUsageToSessionUpdate(event) {
|
|
285
|
+
const sessionKey = event.sessionKey || event.sessionId;
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const result = {};
|
|
288
|
+
if (sessionKey) {
|
|
289
|
+
const inputTokens = event.usage?.input ?? event.usage?.promptTokens ?? 0;
|
|
290
|
+
const outputTokens = event.usage?.output ?? 0;
|
|
291
|
+
result.session = {
|
|
292
|
+
key: sessionKey,
|
|
293
|
+
lastActivityAt: now,
|
|
294
|
+
status: "active",
|
|
295
|
+
totalInputTokens: inputTokens,
|
|
296
|
+
totalOutputTokens: outputTokens,
|
|
297
|
+
};
|
|
298
|
+
if (typeof event.costUsd === "number") {
|
|
299
|
+
result.session.totalCostUsd = event.costUsd;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (typeof event.costUsd === "number" || event.usage) {
|
|
303
|
+
const actionId = `usage-${event.ts}-${event.seq}`;
|
|
304
|
+
result.action = {
|
|
305
|
+
id: actionId,
|
|
306
|
+
runId: actionId,
|
|
307
|
+
sessionKey: sessionKey || "unknown",
|
|
308
|
+
seq: event.seq,
|
|
309
|
+
type: "complete",
|
|
310
|
+
eventType: "system",
|
|
311
|
+
timestamp: event.ts,
|
|
312
|
+
inputTokens: event.usage?.input ?? event.usage?.promptTokens,
|
|
313
|
+
outputTokens: event.usage?.output,
|
|
314
|
+
costUsd: event.costUsd,
|
|
315
|
+
model: event.model,
|
|
316
|
+
provider: event.provider,
|
|
317
|
+
duration: event.durationMs,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { MonitorSession, MonitorActionType, MonitorActionEventType, MonitorAction, MonitorExecEventType, MonitorExecProcessStatus, MonitorExecOutputChunk, MonitorExecEvent, MonitorExecProcess, ChatEvent, AgentEvent, ExecStartedEvent, ExecOutputEvent, ExecCompletedEvent, CollectionStats, DiagnosticUsageEvent, CostUsageTotals, CostUsageSummary, } from "./types.js";
|
|
2
|
+
export { parseSessionKey } from "./types.js";
|
|
3
|
+
export type { ParsedGatewayEvent, SessionInfo, ParsedDiagnosticUsage, } from "./event-parser.js";
|
|
4
|
+
export { sessionInfoToMonitor, chatEventToAction, agentEventToAction, execStartedToEvent, execOutputToEvent, execCompletedToEvent, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./event-parser.js";
|
|
5
|
+
export { CollectionManager } from "./collection-manager.js";
|
|
6
|
+
export { CollectionPersistence } from "./persistence.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseSessionKey } from "./types.js";
|
|
2
|
+
export { sessionInfoToMonitor, chatEventToAction, agentEventToAction, execStartedToEvent, execOutputToEvent, execCompletedToEvent, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./event-parser.js";
|
|
3
|
+
// Collection Manager
|
|
4
|
+
export { CollectionManager } from "./collection-manager.js";
|
|
5
|
+
// Persistence
|
|
6
|
+
export { CollectionPersistence } from "./persistence.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MonitorSession, MonitorAction, MonitorExecEvent } from "./types.js";
|
|
2
|
+
export declare class CollectionPersistence {
|
|
3
|
+
private stateDir;
|
|
4
|
+
private dirty;
|
|
5
|
+
private flushTimer;
|
|
6
|
+
private lastSessionsJson;
|
|
7
|
+
private pendingActions;
|
|
8
|
+
private pendingExecs;
|
|
9
|
+
constructor(stateDir: string);
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
markDirty(): void;
|
|
13
|
+
saveSessions(sessions: MonitorSession[]): void;
|
|
14
|
+
loadSessions(): MonitorSession[];
|
|
15
|
+
queueAction(action: MonitorAction): void;
|
|
16
|
+
loadActions(): MonitorAction[];
|
|
17
|
+
queueExecEvent(event: MonitorExecEvent): void;
|
|
18
|
+
loadExecEvents(): MonitorExecEvent[];
|
|
19
|
+
flush(): void;
|
|
20
|
+
hydrate(): {
|
|
21
|
+
sessions: MonitorSession[];
|
|
22
|
+
actions: MonitorAction[];
|
|
23
|
+
execEvents: MonitorExecEvent[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
4
|
+
const COLLECTIONS_DIR_NAME = "collections";
|
|
5
|
+
const SESSIONS_FILENAME = "sessions.json";
|
|
6
|
+
const ACTIONS_FILENAME = "actions.jsonl";
|
|
7
|
+
const EXECS_FILENAME = "execs.jsonl";
|
|
8
|
+
const MAX_ACTIONS_LINES = 10000;
|
|
9
|
+
const MAX_EXECS_LINES = 20000;
|
|
10
|
+
const FLUSH_INTERVAL_MS = 5000;
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
function resolveDir(stateDir) {
|
|
13
|
+
return path.join(stateDir, COLLECTIONS_DIR_NAME);
|
|
14
|
+
}
|
|
15
|
+
function resolveSessionsPath(stateDir) {
|
|
16
|
+
return path.join(resolveDir(stateDir), SESSIONS_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
function resolveActionsPath(stateDir) {
|
|
19
|
+
return path.join(resolveDir(stateDir), ACTIONS_FILENAME);
|
|
20
|
+
}
|
|
21
|
+
function resolveExecsPath(stateDir) {
|
|
22
|
+
return path.join(resolveDir(stateDir), EXECS_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
function ensureDir(stateDir) {
|
|
25
|
+
const dir = resolveDir(stateDir);
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeAtomic(filePath, content) {
|
|
31
|
+
const tmpPath = filePath + ".tmp";
|
|
32
|
+
try {
|
|
33
|
+
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
34
|
+
fs.renameSync(tmpPath, filePath);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(tmpPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore cleanup failure
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function appendLine(filePath, line) {
|
|
47
|
+
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
|
48
|
+
}
|
|
49
|
+
function capJsonlFile(filePath, maxLines) {
|
|
50
|
+
if (!fs.existsSync(filePath))
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
54
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
55
|
+
if (lines.length <= maxLines)
|
|
56
|
+
return;
|
|
57
|
+
const kept = lines.slice(-maxLines);
|
|
58
|
+
writeAtomic(filePath, kept.join("\n") + "\n");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore errors during cap
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ─── Persistence Class ───────────────────────────────────────────────────────
|
|
65
|
+
export class CollectionPersistence {
|
|
66
|
+
stateDir;
|
|
67
|
+
dirty = false;
|
|
68
|
+
flushTimer = null;
|
|
69
|
+
// Cached data for tracking changes
|
|
70
|
+
lastSessionsJson = "";
|
|
71
|
+
pendingActions = [];
|
|
72
|
+
pendingExecs = [];
|
|
73
|
+
constructor(stateDir) {
|
|
74
|
+
this.stateDir = stateDir;
|
|
75
|
+
}
|
|
76
|
+
start() {
|
|
77
|
+
ensureDir(this.stateDir);
|
|
78
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
if (this.flushTimer) {
|
|
82
|
+
clearInterval(this.flushTimer);
|
|
83
|
+
this.flushTimer = null;
|
|
84
|
+
}
|
|
85
|
+
this.flush();
|
|
86
|
+
}
|
|
87
|
+
markDirty() {
|
|
88
|
+
this.dirty = true;
|
|
89
|
+
}
|
|
90
|
+
// ─── Sessions (JSON rewrite) ──────────────────────────────────────────────
|
|
91
|
+
saveSessions(sessions) {
|
|
92
|
+
const json = JSON.stringify(sessions, null, 2);
|
|
93
|
+
if (json === this.lastSessionsJson)
|
|
94
|
+
return;
|
|
95
|
+
this.lastSessionsJson = json;
|
|
96
|
+
this.dirty = true;
|
|
97
|
+
}
|
|
98
|
+
loadSessions() {
|
|
99
|
+
const filePath = resolveSessionsPath(this.stateDir);
|
|
100
|
+
if (!fs.existsSync(filePath))
|
|
101
|
+
return [];
|
|
102
|
+
try {
|
|
103
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
const parsed = JSON.parse(content);
|
|
105
|
+
if (Array.isArray(parsed)) {
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ─── Actions (JSONL append) ───────────────────────────────────────────────
|
|
115
|
+
queueAction(action) {
|
|
116
|
+
this.pendingActions.push(action);
|
|
117
|
+
this.dirty = true;
|
|
118
|
+
}
|
|
119
|
+
loadActions() {
|
|
120
|
+
const filePath = resolveActionsPath(this.stateDir);
|
|
121
|
+
if (!fs.existsSync(filePath))
|
|
122
|
+
return [];
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
125
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
126
|
+
const actions = [];
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(line);
|
|
130
|
+
if (parsed && typeof parsed.id === "string") {
|
|
131
|
+
actions.push(parsed);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Skip malformed lines
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return actions;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ─── Execs (JSONL append) ─────────────────────────────────────────────────
|
|
145
|
+
queueExecEvent(event) {
|
|
146
|
+
this.pendingExecs.push(event);
|
|
147
|
+
this.dirty = true;
|
|
148
|
+
}
|
|
149
|
+
loadExecEvents() {
|
|
150
|
+
const filePath = resolveExecsPath(this.stateDir);
|
|
151
|
+
if (!fs.existsSync(filePath))
|
|
152
|
+
return [];
|
|
153
|
+
try {
|
|
154
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
155
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
156
|
+
const events = [];
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(line);
|
|
160
|
+
if (parsed && typeof parsed.id === "string") {
|
|
161
|
+
events.push(parsed);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Skip malformed lines
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return events;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ─── Flush ────────────────────────────────────────────────────────────────
|
|
175
|
+
flush() {
|
|
176
|
+
if (!this.dirty && this.pendingActions.length === 0 && this.pendingExecs.length === 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
ensureDir(this.stateDir);
|
|
180
|
+
// Write sessions if changed
|
|
181
|
+
if (this.lastSessionsJson) {
|
|
182
|
+
const filePath = resolveSessionsPath(this.stateDir);
|
|
183
|
+
writeAtomic(filePath, this.lastSessionsJson);
|
|
184
|
+
}
|
|
185
|
+
// Append pending actions
|
|
186
|
+
if (this.pendingActions.length > 0) {
|
|
187
|
+
const filePath = resolveActionsPath(this.stateDir);
|
|
188
|
+
for (const action of this.pendingActions) {
|
|
189
|
+
appendLine(filePath, JSON.stringify(action));
|
|
190
|
+
}
|
|
191
|
+
this.pendingActions = [];
|
|
192
|
+
capJsonlFile(filePath, MAX_ACTIONS_LINES);
|
|
193
|
+
}
|
|
194
|
+
// Append pending exec events
|
|
195
|
+
if (this.pendingExecs.length > 0) {
|
|
196
|
+
const filePath = resolveExecsPath(this.stateDir);
|
|
197
|
+
for (const event of this.pendingExecs) {
|
|
198
|
+
appendLine(filePath, JSON.stringify(event));
|
|
199
|
+
}
|
|
200
|
+
this.pendingExecs = [];
|
|
201
|
+
capJsonlFile(filePath, MAX_EXECS_LINES);
|
|
202
|
+
}
|
|
203
|
+
this.dirty = false;
|
|
204
|
+
}
|
|
205
|
+
// ─── Full Hydration ───────────────────────────────────────────────────────
|
|
206
|
+
hydrate() {
|
|
207
|
+
return {
|
|
208
|
+
sessions: this.loadSessions(),
|
|
209
|
+
actions: this.loadActions(),
|
|
210
|
+
execEvents: this.loadExecEvents(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|