bosun 0.41.0 → 0.41.2
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/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -1,94 +1,340 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* runtime-accumulator.mjs — Persists runtime and session
|
|
2
|
+
* runtime-accumulator.mjs — Persists runtime and completed session totals across restarts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
* - Session history
|
|
4
|
+
* Source of truth for completed-session lifetime stats is an append-only JSONL log written
|
|
5
|
+
* synchronously on every terminal session completion. A compact snapshot file is also kept as a
|
|
6
|
+
* best-effort cache for faster startup and legacy compatibility, but restart recovery never
|
|
7
|
+
* depends on the snapshot being current.
|
|
9
8
|
*
|
|
10
9
|
* @module runtime-accumulator
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
closeSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
fsyncSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
openSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
writeSync,
|
|
22
|
+
} from "node:fs";
|
|
14
23
|
import { resolve, dirname } from "node:path";
|
|
15
24
|
import { fileURLToPath } from "node:url";
|
|
16
25
|
|
|
17
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const
|
|
19
|
-
const
|
|
27
|
+
const DEFAULT_CACHE_DIR = resolve(__dirname, "..", ".cache");
|
|
28
|
+
const SNAPSHOT_FILE_NAME = "runtime-accumulator.json";
|
|
29
|
+
const SESSION_LOG_FILE_NAME = "session-accumulator.jsonl";
|
|
30
|
+
const MAX_SESSION_TOKENS = 100;
|
|
31
|
+
const MAX_COMPLETED_SESSIONS = 500;
|
|
32
|
+
|
|
33
|
+
let _cacheDir = DEFAULT_CACHE_DIR;
|
|
34
|
+
let _runtimeFile = resolve(_cacheDir, SNAPSHOT_FILE_NAME);
|
|
35
|
+
let _sessionLogFile = resolve(_cacheDir, SESSION_LOG_FILE_NAME);
|
|
20
36
|
|
|
21
37
|
const DEFAULT_STATE = {
|
|
22
38
|
runtimeMs: 0,
|
|
23
39
|
totalCostUsd: 0,
|
|
24
40
|
sessionTokens: [],
|
|
25
41
|
completedSessions: [],
|
|
42
|
+
taskLifetimeTotals: {},
|
|
26
43
|
lastUpdated: null,
|
|
27
44
|
startedAt: null,
|
|
28
45
|
};
|
|
29
46
|
|
|
30
|
-
let _state =
|
|
47
|
+
let _state = cloneDefaultState();
|
|
31
48
|
let _initialized = false;
|
|
32
49
|
let _lastSaveTime = 0;
|
|
50
|
+
let _saveHooksInstalled = false;
|
|
51
|
+
let _seenSessionKeys = new Set();
|
|
52
|
+
|
|
33
53
|
const SAVE_INTERVAL_MS = 5000;
|
|
54
|
+
const SESSION_ACCUMULATION_LISTENERS = new Set();
|
|
34
55
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
function cloneDefaultState() {
|
|
57
|
+
return {
|
|
58
|
+
...DEFAULT_STATE,
|
|
59
|
+
sessionTokens: [],
|
|
60
|
+
completedSessions: [],
|
|
61
|
+
taskLifetimeTotals: {},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function configureCachePaths(cacheDir = DEFAULT_CACHE_DIR) {
|
|
66
|
+
_cacheDir = resolve(cacheDir || DEFAULT_CACHE_DIR);
|
|
67
|
+
_runtimeFile = resolve(_cacheDir, SNAPSHOT_FILE_NAME);
|
|
68
|
+
_sessionLogFile = resolve(_cacheDir, SESSION_LOG_FILE_NAME);
|
|
69
|
+
}
|
|
38
70
|
|
|
71
|
+
function ensureCacheDir() {
|
|
39
72
|
try {
|
|
40
|
-
mkdirSync(
|
|
41
|
-
} catch {
|
|
73
|
+
mkdirSync(_cacheDir, { recursive: true });
|
|
74
|
+
} catch {
|
|
75
|
+
/* best effort */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toFiniteNumber(value, fallback = 0) {
|
|
80
|
+
const parsed = Number(value);
|
|
81
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function syncGlobals() {
|
|
85
|
+
globalThis.__bosun_runtimeMs = _state.runtimeMs;
|
|
86
|
+
globalThis.__bosun_totalCostUsd = _state.totalCostUsd;
|
|
87
|
+
globalThis.__bosun_sessionTokens = getSessionTokens;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeCompletedSession(session = {}) {
|
|
91
|
+
const taskId = String(session.taskId || session.id || session.sessionId || "").trim();
|
|
92
|
+
const endedAt = toFiniteNumber(session.endedAt, Date.now());
|
|
93
|
+
const startedAt = toFiniteNumber(session.startedAt, endedAt);
|
|
94
|
+
const inputTokens = toFiniteNumber(
|
|
95
|
+
session.inputTokens ?? session.prompt_tokens ?? session.promptTokens ?? session.input_tokens,
|
|
96
|
+
0,
|
|
97
|
+
);
|
|
98
|
+
const outputTokens = toFiniteNumber(
|
|
99
|
+
session.outputTokens ?? session.completion_tokens ?? session.completionTokens ?? session.output_tokens,
|
|
100
|
+
0,
|
|
101
|
+
);
|
|
102
|
+
const tokenCount = toFiniteNumber(
|
|
103
|
+
session.tokenCount ?? session.totalTokens ?? session.total_tokens ?? session.tokens ?? (inputTokens + outputTokens),
|
|
104
|
+
inputTokens + outputTokens,
|
|
105
|
+
);
|
|
106
|
+
const durationMs = Math.max(
|
|
107
|
+
0,
|
|
108
|
+
toFiniteNumber(session.durationMs, endedAt > startedAt ? endedAt - startedAt : 0),
|
|
109
|
+
);
|
|
110
|
+
const costUsd = Math.max(
|
|
111
|
+
0,
|
|
112
|
+
toFiniteNumber(session.costUsd ?? session.cost_usd ?? session.cost ?? session.total_cost ?? session.usd, 0),
|
|
113
|
+
);
|
|
114
|
+
const stableId = String(session.id || session.sessionId || `${taskId || "session"}:${startedAt}:${endedAt}`).trim();
|
|
115
|
+
const sessionKey = String(
|
|
116
|
+
session.sessionKey || `${taskId || "task"}:${stableId}:${startedAt}:${endedAt}`,
|
|
117
|
+
).trim();
|
|
42
118
|
|
|
119
|
+
return {
|
|
120
|
+
type: "completed_session",
|
|
121
|
+
sessionKey,
|
|
122
|
+
id: stableId,
|
|
123
|
+
taskId,
|
|
124
|
+
taskTitle: String(session.taskTitle || "").trim() || null,
|
|
125
|
+
executor: String(session.executor || "").trim() || null,
|
|
126
|
+
model: String(session.model || "").trim() || null,
|
|
127
|
+
startedAt,
|
|
128
|
+
endedAt,
|
|
129
|
+
durationMs,
|
|
130
|
+
status: String(session.status || "completed").trim() || "completed",
|
|
131
|
+
tokenCount,
|
|
132
|
+
inputTokens,
|
|
133
|
+
outputTokens,
|
|
134
|
+
costUsd,
|
|
135
|
+
recordedAt: String(session.recordedAt || new Date().toISOString()),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildSessionDedupKey(record) {
|
|
140
|
+
return String(record?.sessionKey || "").trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cloneLifetimeTotals(totals) {
|
|
144
|
+
if (!totals) return null;
|
|
145
|
+
return { ...totals };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function applyCompletedSessionRecord(record) {
|
|
149
|
+
if (!record?.taskId) return null;
|
|
150
|
+
const dedupKey = buildSessionDedupKey(record);
|
|
151
|
+
if (!dedupKey || _seenSessionKeys.has(dedupKey)) {
|
|
152
|
+
return record.taskId ? cloneLifetimeTotals(_state.taskLifetimeTotals[record.taskId]) : null;
|
|
153
|
+
}
|
|
154
|
+
_seenSessionKeys.add(dedupKey);
|
|
155
|
+
|
|
156
|
+
_state.completedSessions.push({ ...record });
|
|
157
|
+
if (_state.completedSessions.length > MAX_COMPLETED_SESSIONS) {
|
|
158
|
+
_state.completedSessions = _state.completedSessions.slice(-MAX_COMPLETED_SESSIONS);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_state.sessionTokens.push({
|
|
162
|
+
id: record.id,
|
|
163
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
|
|
164
|
+
});
|
|
165
|
+
if (_state.sessionTokens.length > MAX_SESSION_TOKENS) {
|
|
166
|
+
_state.sessionTokens = _state.sessionTokens.slice(-MAX_SESSION_TOKENS);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const currentTotals = _state.taskLifetimeTotals[record.taskId] || {
|
|
170
|
+
taskId: record.taskId,
|
|
171
|
+
taskTitle: record.taskTitle,
|
|
172
|
+
attemptsCount: 0,
|
|
173
|
+
tokenCount: 0,
|
|
174
|
+
inputTokens: 0,
|
|
175
|
+
outputTokens: 0,
|
|
176
|
+
durationMs: 0,
|
|
177
|
+
lastSessionId: null,
|
|
178
|
+
lastSessionEndedAt: null,
|
|
179
|
+
lastStatus: null,
|
|
180
|
+
updatedAt: null,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const nextTotals = {
|
|
184
|
+
...currentTotals,
|
|
185
|
+
taskId: record.taskId,
|
|
186
|
+
taskTitle: record.taskTitle || currentTotals.taskTitle || null,
|
|
187
|
+
attemptsCount: currentTotals.attemptsCount + 1,
|
|
188
|
+
tokenCount: currentTotals.tokenCount + record.tokenCount,
|
|
189
|
+
inputTokens: currentTotals.inputTokens + record.inputTokens,
|
|
190
|
+
outputTokens: currentTotals.outputTokens + record.outputTokens,
|
|
191
|
+
durationMs: currentTotals.durationMs + record.durationMs,
|
|
192
|
+
lastSessionId: record.id,
|
|
193
|
+
lastSessionEndedAt: record.endedAt,
|
|
194
|
+
lastStatus: record.status,
|
|
195
|
+
updatedAt: record.recordedAt,
|
|
196
|
+
};
|
|
197
|
+
_state.taskLifetimeTotals[record.taskId] = nextTotals;
|
|
198
|
+
|
|
199
|
+
_state.runtimeMs += record.durationMs;
|
|
200
|
+
_state.totalCostUsd += record.costUsd;
|
|
201
|
+
_state.lastUpdated = record.recordedAt;
|
|
202
|
+
syncGlobals();
|
|
203
|
+
return cloneLifetimeTotals(nextTotals);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function loadLegacySnapshot() {
|
|
207
|
+
if (!existsSync(_runtimeFile)) return;
|
|
43
208
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
209
|
+
const raw = readFileSync(_runtimeFile, "utf8");
|
|
210
|
+
const parsed = JSON.parse(raw);
|
|
211
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
212
|
+
|
|
213
|
+
_state.startedAt = parsed.startedAt || _state.startedAt;
|
|
214
|
+
_state.lastUpdated = parsed.lastUpdated || _state.lastUpdated;
|
|
215
|
+
_state.sessionTokens = Array.isArray(parsed.sessionTokens) ? parsed.sessionTokens : [];
|
|
216
|
+
|
|
217
|
+
const completedSessions = Array.isArray(parsed.completedSessions)
|
|
218
|
+
? parsed.completedSessions
|
|
219
|
+
: Array.isArray(parsed.sessions)
|
|
220
|
+
? parsed.sessions
|
|
221
|
+
: [];
|
|
222
|
+
for (const session of completedSessions) {
|
|
223
|
+
applyCompletedSessionRecord(normalizeCompletedSession(session));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (parsed.taskLifetimeTotals && typeof parsed.taskLifetimeTotals === "object") {
|
|
227
|
+
for (const [taskId, totals] of Object.entries(parsed.taskLifetimeTotals)) {
|
|
228
|
+
if (!_state.taskLifetimeTotals[taskId]) {
|
|
229
|
+
_state.taskLifetimeTotals[taskId] = { ...totals };
|
|
230
|
+
}
|
|
54
231
|
}
|
|
55
232
|
}
|
|
56
233
|
} catch (err) {
|
|
57
|
-
console.warn(`[runtime-accumulator] failed to load
|
|
234
|
+
console.warn(`[runtime-accumulator] failed to load snapshot: ${err.message}`);
|
|
58
235
|
}
|
|
236
|
+
}
|
|
59
237
|
|
|
238
|
+
function loadSessionLog() {
|
|
239
|
+
if (!existsSync(_sessionLogFile)) return;
|
|
240
|
+
try {
|
|
241
|
+
const raw = readFileSync(_sessionLogFile, "utf8");
|
|
242
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (!trimmed) continue;
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(trimmed);
|
|
247
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
248
|
+
applyCompletedSessionRecord(normalizeCompletedSession(parsed));
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore malformed lines to preserve append-only recovery.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn(`[runtime-accumulator] failed to load session log: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function loadState() {
|
|
259
|
+
if (_initialized) return;
|
|
260
|
+
_initialized = true;
|
|
261
|
+
ensureCacheDir();
|
|
262
|
+
loadLegacySnapshot();
|
|
263
|
+
loadSessionLog();
|
|
60
264
|
_state.startedAt = _state.startedAt || Date.now();
|
|
265
|
+
syncGlobals();
|
|
61
266
|
}
|
|
62
267
|
|
|
63
|
-
function saveState() {
|
|
268
|
+
function saveState(force = false) {
|
|
64
269
|
const now = Date.now();
|
|
65
|
-
if (now - _lastSaveTime < SAVE_INTERVAL_MS) return;
|
|
270
|
+
if (!force && now - _lastSaveTime < SAVE_INTERVAL_MS) return;
|
|
66
271
|
_lastSaveTime = now;
|
|
67
272
|
|
|
68
273
|
try {
|
|
69
|
-
|
|
274
|
+
ensureCacheDir();
|
|
70
275
|
const payload = {
|
|
71
276
|
runtimeMs: _state.runtimeMs,
|
|
72
277
|
totalCostUsd: _state.totalCostUsd,
|
|
73
|
-
sessionTokens: _state.sessionTokens.slice(-
|
|
74
|
-
completedSessions: _state.completedSessions.slice(-
|
|
75
|
-
|
|
278
|
+
sessionTokens: _state.sessionTokens.slice(-MAX_SESSION_TOKENS),
|
|
279
|
+
completedSessions: _state.completedSessions.slice(-MAX_COMPLETED_SESSIONS),
|
|
280
|
+
taskLifetimeTotals: _state.taskLifetimeTotals,
|
|
281
|
+
lastUpdated: _state.lastUpdated,
|
|
76
282
|
startedAt: _state.startedAt,
|
|
77
283
|
};
|
|
78
|
-
writeFileSync(
|
|
284
|
+
writeFileSync(_runtimeFile, JSON.stringify(payload, null, 2), "utf8");
|
|
79
285
|
} catch (err) {
|
|
80
286
|
console.warn(`[runtime-accumulator] failed to save state: ${err.message}`);
|
|
81
287
|
}
|
|
82
288
|
}
|
|
83
289
|
|
|
290
|
+
function appendCompletedSessionRecord(record) {
|
|
291
|
+
ensureCacheDir();
|
|
292
|
+
const fd = openSync(_sessionLogFile, "a");
|
|
293
|
+
try {
|
|
294
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
295
|
+
writeSync(fd, line, undefined, "utf8");
|
|
296
|
+
fsyncSync(fd);
|
|
297
|
+
} finally {
|
|
298
|
+
closeSync(fd);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function emitSessionAccumulated(taskId, session, totals) {
|
|
303
|
+
if (!taskId || SESSION_ACCUMULATION_LISTENERS.size === 0) return;
|
|
304
|
+
const payload = {
|
|
305
|
+
type: "session-accumulated",
|
|
306
|
+
taskId,
|
|
307
|
+
session: { ...session },
|
|
308
|
+
totals: cloneLifetimeTotals(totals),
|
|
309
|
+
ts: Date.now(),
|
|
310
|
+
};
|
|
311
|
+
for (const listener of SESSION_ACCUMULATION_LISTENERS) {
|
|
312
|
+
try {
|
|
313
|
+
listener(payload);
|
|
314
|
+
} catch {
|
|
315
|
+
/* best effort */
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function addSessionAccumulationListener(listener) {
|
|
321
|
+
if (typeof listener !== "function") return () => {};
|
|
322
|
+
SESSION_ACCUMULATION_LISTENERS.add(listener);
|
|
323
|
+
return () => SESSION_ACCUMULATION_LISTENERS.delete(listener);
|
|
324
|
+
}
|
|
325
|
+
|
|
84
326
|
export function initRuntimeAccumulator() {
|
|
85
327
|
loadState();
|
|
86
328
|
_state.startedAt = Date.now();
|
|
329
|
+
syncGlobals();
|
|
87
330
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
331
|
+
if (!_saveHooksInstalled) {
|
|
332
|
+
_saveHooksInstalled = true;
|
|
333
|
+
setInterval(() => saveState(false), SAVE_INTERVAL_MS).unref?.();
|
|
334
|
+
process.on("exit", () => saveState(true));
|
|
335
|
+
process.on("SIGINT", () => saveState(true));
|
|
336
|
+
process.on("SIGTERM", () => saveState(true));
|
|
337
|
+
}
|
|
92
338
|
|
|
93
339
|
return {
|
|
94
340
|
runtimeMs: _state.runtimeMs,
|
|
@@ -104,71 +350,106 @@ export function getRuntimeStats() {
|
|
|
104
350
|
totalCostUsd: _state.totalCostUsd,
|
|
105
351
|
sessionCount: _state.completedSessions.length,
|
|
106
352
|
startedAt: _state.startedAt,
|
|
353
|
+
lastUpdated: _state.lastUpdated,
|
|
107
354
|
};
|
|
108
355
|
}
|
|
109
356
|
|
|
110
357
|
export function addRuntime(ms) {
|
|
111
358
|
loadState();
|
|
112
|
-
_state.runtimeMs +=
|
|
113
|
-
|
|
359
|
+
_state.runtimeMs += Math.max(0, toFiniteNumber(ms, 0));
|
|
360
|
+
_state.lastUpdated = new Date().toISOString();
|
|
361
|
+
syncGlobals();
|
|
362
|
+
saveState(false);
|
|
114
363
|
}
|
|
115
364
|
|
|
116
365
|
export function addCost(usd) {
|
|
117
366
|
loadState();
|
|
118
|
-
_state.totalCostUsd +=
|
|
119
|
-
|
|
367
|
+
_state.totalCostUsd += Math.max(0, toFiniteNumber(usd, 0));
|
|
368
|
+
_state.lastUpdated = new Date().toISOString();
|
|
369
|
+
syncGlobals();
|
|
370
|
+
saveState(false);
|
|
120
371
|
}
|
|
121
372
|
|
|
122
373
|
export function addCompletedSession(session) {
|
|
123
374
|
loadState();
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
model: session.model,
|
|
130
|
-
startedAt: session.startedAt,
|
|
131
|
-
endedAt: session.endedAt || Date.now(),
|
|
132
|
-
durationMs: session.durationMs || (session.endedAt ? session.endedAt - session.startedAt : 0),
|
|
133
|
-
costUsd: session.costUsd || 0,
|
|
134
|
-
status: session.status || "completed",
|
|
135
|
-
};
|
|
136
|
-
_state.completedSessions.push(token);
|
|
137
|
-
|
|
138
|
-
if (token.costUsd) {
|
|
139
|
-
_state.totalCostUsd += token.costUsd;
|
|
375
|
+
const record = normalizeCompletedSession(session);
|
|
376
|
+
if (!record.taskId) return record;
|
|
377
|
+
const dedupKey = buildSessionDedupKey(record);
|
|
378
|
+
if (dedupKey && _seenSessionKeys.has(dedupKey)) {
|
|
379
|
+
return record;
|
|
140
380
|
}
|
|
141
|
-
if (token.durationMs) {
|
|
142
|
-
_state.runtimeMs += token.durationMs;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
_state.sessionTokens.push({
|
|
146
|
-
id: token.id,
|
|
147
|
-
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
|
|
148
|
-
});
|
|
149
381
|
|
|
150
|
-
|
|
151
|
-
|
|
382
|
+
appendCompletedSessionRecord(record);
|
|
383
|
+
const totals = applyCompletedSessionRecord(record);
|
|
384
|
+
saveState(true);
|
|
385
|
+
emitSessionAccumulated(record.taskId, record, totals);
|
|
386
|
+
return record;
|
|
152
387
|
}
|
|
153
388
|
|
|
154
389
|
export function getSessionTokens() {
|
|
155
390
|
loadState();
|
|
156
391
|
const now = Date.now();
|
|
157
392
|
return _state.sessionTokens
|
|
158
|
-
.filter((
|
|
159
|
-
.map((
|
|
393
|
+
.filter((token) => toFiniteNumber(token?.expiresAt, 0) > now)
|
|
394
|
+
.map((token) => token.id);
|
|
160
395
|
}
|
|
161
396
|
|
|
162
397
|
export function getCompletedSessions(limit = 100) {
|
|
163
398
|
loadState();
|
|
164
|
-
return _state.completedSessions
|
|
399
|
+
return _state.completedSessions
|
|
400
|
+
.slice(-Math.max(0, Number(limit) || 0))
|
|
401
|
+
.map((entry) => ({ ...entry }));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function getTaskLifetimeTotals(taskId) {
|
|
405
|
+
loadState();
|
|
406
|
+
const normalizedTaskId = String(taskId || "").trim();
|
|
407
|
+
if (!normalizedTaskId) return null;
|
|
408
|
+
const totals = _state.taskLifetimeTotals[normalizedTaskId];
|
|
409
|
+
if (!totals) {
|
|
410
|
+
return {
|
|
411
|
+
taskId: normalizedTaskId,
|
|
412
|
+
taskTitle: null,
|
|
413
|
+
attemptsCount: 0,
|
|
414
|
+
tokenCount: 0,
|
|
415
|
+
inputTokens: 0,
|
|
416
|
+
outputTokens: 0,
|
|
417
|
+
durationMs: 0,
|
|
418
|
+
lastSessionId: null,
|
|
419
|
+
lastSessionEndedAt: null,
|
|
420
|
+
lastStatus: null,
|
|
421
|
+
updatedAt: null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return cloneLifetimeTotals(totals);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function withTaskLifetimeTotals(task) {
|
|
428
|
+
if (!task || typeof task !== "object") return task;
|
|
429
|
+
const taskId = String(task.id || task.taskId || "").trim();
|
|
430
|
+
if (!taskId) return task;
|
|
431
|
+
const lifetimeTotals = getTaskLifetimeTotals(taskId);
|
|
432
|
+
return {
|
|
433
|
+
...task,
|
|
434
|
+
lifetimeTotals,
|
|
435
|
+
meta: {
|
|
436
|
+
...(task.meta || {}),
|
|
437
|
+
lifetimeTotals,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
165
440
|
}
|
|
166
441
|
|
|
167
442
|
export function clearCompletedSessions() {
|
|
168
443
|
loadState();
|
|
169
|
-
_state
|
|
170
|
-
|
|
171
|
-
|
|
444
|
+
_state = cloneDefaultState();
|
|
445
|
+
_seenSessionKeys = new Set();
|
|
446
|
+
try {
|
|
447
|
+
unlinkSync(_sessionLogFile);
|
|
448
|
+
} catch {
|
|
449
|
+
/* best effort */
|
|
450
|
+
}
|
|
451
|
+
syncGlobals();
|
|
452
|
+
saveState(true);
|
|
172
453
|
}
|
|
173
454
|
|
|
174
455
|
export function exportRuntimeData() {
|
|
@@ -179,28 +460,39 @@ export function exportRuntimeData() {
|
|
|
179
460
|
sessionCount: _state.completedSessions.length,
|
|
180
461
|
startedAt: _state.startedAt,
|
|
181
462
|
lastUpdated: _state.lastUpdated,
|
|
182
|
-
|
|
463
|
+
taskLifetimeTotals: { ..._state.taskLifetimeTotals },
|
|
464
|
+
sessions: _state.completedSessions.map((entry) => ({ ...entry })),
|
|
183
465
|
};
|
|
184
466
|
}
|
|
185
467
|
|
|
186
468
|
export function importRuntimeData(data) {
|
|
187
469
|
if (!data || typeof data !== "object") return false;
|
|
188
|
-
|
|
189
470
|
loadState();
|
|
190
|
-
|
|
471
|
+
if (Array.isArray(data.sessions)) {
|
|
472
|
+
for (const session of data.sessions) {
|
|
473
|
+
addCompletedSession(session);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
191
476
|
if (typeof data.runtimeMs === "number") {
|
|
192
477
|
_state.runtimeMs = Math.max(_state.runtimeMs, data.runtimeMs);
|
|
193
478
|
}
|
|
194
479
|
if (typeof data.totalCostUsd === "number") {
|
|
195
480
|
_state.totalCostUsd = Math.max(_state.totalCostUsd, data.totalCostUsd);
|
|
196
481
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
_state.completedSessions.push(session);
|
|
200
|
-
}
|
|
201
|
-
_state.completedSessions = _state.completedSessions.slice(-500);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
saveState();
|
|
482
|
+
syncGlobals();
|
|
483
|
+
saveState(true);
|
|
205
484
|
return true;
|
|
206
485
|
}
|
|
486
|
+
|
|
487
|
+
export function getSessionAccumulatorLogPath() {
|
|
488
|
+
return _sessionLogFile;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function _resetRuntimeAccumulatorForTests(options = {}) {
|
|
492
|
+
configureCachePaths(options.cacheDir || DEFAULT_CACHE_DIR);
|
|
493
|
+
_state = cloneDefaultState();
|
|
494
|
+
_initialized = false;
|
|
495
|
+
_lastSaveTime = 0;
|
|
496
|
+
_seenSessionKeys = new Set();
|
|
497
|
+
syncGlobals();
|
|
498
|
+
}
|