oh-my-opencode-dashboard 0.0.1 → 0.0.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.
@@ -0,0 +1,491 @@
1
+ import * as fs from "node:fs"
2
+ import * as os from "node:os"
3
+ import * as path from "node:path"
4
+ import { describe, expect, it } from "vitest"
5
+ import { deriveTimeSeriesActivity } from "./timeseries"
6
+ import { getStorageRoots } from "./session"
7
+
8
+ function mkStorageRoot(): string {
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "omo-storage-"))
10
+ fs.mkdirSync(path.join(root, "session"), { recursive: true })
11
+ fs.mkdirSync(path.join(root, "message"), { recursive: true })
12
+ fs.mkdirSync(path.join(root, "part"), { recursive: true })
13
+ return root
14
+ }
15
+
16
+ function writeMessageMeta(opts: {
17
+ messageDir: string
18
+ messageId: string
19
+ meta: Record<string, unknown>
20
+ }): void {
21
+ fs.mkdirSync(opts.messageDir, { recursive: true })
22
+ fs.writeFileSync(
23
+ path.join(opts.messageDir, `${opts.messageId}.json`),
24
+ JSON.stringify({ id: opts.messageId, sessionID: "", role: "assistant", ...opts.meta }),
25
+ "utf8"
26
+ )
27
+ }
28
+
29
+ function writePartJson(opts: { partDir: string; fileName: string; value: unknown }): void {
30
+ fs.mkdirSync(opts.partDir, { recursive: true })
31
+ fs.writeFileSync(path.join(opts.partDir, opts.fileName), JSON.stringify(opts.value), "utf8")
32
+ }
33
+
34
+ function writeMalformedPart(opts: { partDir: string; fileName: string; value: string }): void {
35
+ fs.mkdirSync(opts.partDir, { recursive: true })
36
+ fs.writeFileSync(path.join(opts.partDir, opts.fileName), opts.value, "utf8")
37
+ }
38
+
39
+ function getSeries(result: ReturnType<typeof deriveTimeSeriesActivity>, id: string) {
40
+ const match = result.series.find((series) => series.id === id)
41
+ expect(match).toBeDefined()
42
+ return match!
43
+ }
44
+
45
+ describe("deriveTimeSeriesActivity", () => {
46
+ it("returns fixed series metadata and zero-filled arrays when no session exists", () => {
47
+ const storageRoot = mkStorageRoot()
48
+ const storage = getStorageRoots(storageRoot)
49
+
50
+ // #given
51
+ const nowMs = 173_456
52
+
53
+ // #when
54
+ const result = deriveTimeSeriesActivity({ storage, mainSessionId: null, nowMs })
55
+
56
+ // #then
57
+ expect(result.windowMs).toBe(300_000)
58
+ expect(result.bucketMs).toBe(2_000)
59
+ expect(result.buckets).toBe(150)
60
+ expect(result.serverNowMs).toBe(nowMs)
61
+ expect(result.anchorMs).toBe(Math.floor(nowMs / 2_000) * 2_000)
62
+
63
+ expect(result.series.map((series) => series.id)).toEqual([
64
+ "overall-main",
65
+ "agent:sisyphus",
66
+ "agent:prometheus",
67
+ "agent:atlas",
68
+ "background-total",
69
+ ])
70
+ expect(result.series.map((series) => series.label)).toEqual([
71
+ "Overall",
72
+ "Sisyphus",
73
+ "Prometheus",
74
+ "Atlas",
75
+ "Background tasks (total)",
76
+ ])
77
+ expect(result.series.map((series) => series.tone)).toEqual([
78
+ "muted",
79
+ "teal",
80
+ "red",
81
+ "green",
82
+ "muted",
83
+ ])
84
+
85
+ for (const series of result.series) {
86
+ expect(series.values.length).toBe(result.buckets)
87
+ expect(series.values.every((value) => value === 0)).toBe(true)
88
+ }
89
+ })
90
+
91
+ it("buckets tool parts by message time and canonical agent mapping", () => {
92
+ const storageRoot = mkStorageRoot()
93
+ const storage = getStorageRoots(storageRoot)
94
+ const mainSessionId = "ses_main"
95
+ const messageDir = path.join(storage.message, mainSessionId)
96
+
97
+ // #given
98
+ writeMessageMeta({
99
+ messageDir,
100
+ messageId: "msg_a",
101
+ meta: { sessionID: mainSessionId, agent: "Sisyphus v2", time: { created: 0 } },
102
+ })
103
+ const partDirA = path.join(storage.part, "msg_a")
104
+ writePartJson({
105
+ partDir: partDirA,
106
+ fileName: "part_1.json",
107
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
108
+ })
109
+ writePartJson({
110
+ partDir: partDirA,
111
+ fileName: "part_2.json",
112
+ value: { type: "tool", tool: "grep", state: { status: "completed", input: {} } },
113
+ })
114
+ writePartJson({
115
+ partDir: partDirA,
116
+ fileName: "part_3.json",
117
+ value: { type: "text" },
118
+ })
119
+ writeMalformedPart({ partDir: partDirA, fileName: "part_4.json", value: "{not json" })
120
+
121
+ writeMessageMeta({
122
+ messageDir,
123
+ messageId: "msg_b",
124
+ meta: { sessionID: mainSessionId, agent: "PROMETHEUS", time: { created: 1_999 } },
125
+ })
126
+ writePartJson({
127
+ partDir: path.join(storage.part, "msg_b"),
128
+ fileName: "part_1.json",
129
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
130
+ })
131
+
132
+ writeMessageMeta({
133
+ messageDir,
134
+ messageId: "msg_c",
135
+ meta: { sessionID: mainSessionId, agent: "Atlas", time: { created: 2_000 } },
136
+ })
137
+ writePartJson({
138
+ partDir: path.join(storage.part, "msg_c"),
139
+ fileName: "part_1.json",
140
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
141
+ })
142
+
143
+ writeMessageMeta({
144
+ messageDir,
145
+ messageId: "msg_d",
146
+ meta: { sessionID: mainSessionId, agent: "unknown", time: { created: 8_000 } },
147
+ })
148
+ writePartJson({
149
+ partDir: path.join(storage.part, "msg_d"),
150
+ fileName: "part_1.json",
151
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
152
+ })
153
+
154
+ writeMessageMeta({
155
+ messageDir,
156
+ messageId: "msg_e",
157
+ meta: { sessionID: mainSessionId, agent: "Sisyphus", time: { created: 9_999 } },
158
+ })
159
+ writePartJson({
160
+ partDir: path.join(storage.part, "msg_e"),
161
+ fileName: "part_1.json",
162
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
163
+ })
164
+
165
+ writeMessageMeta({
166
+ messageDir,
167
+ messageId: "msg_f",
168
+ meta: { sessionID: mainSessionId, agent: "Sisyphus", time: { created: 10_000 } },
169
+ })
170
+ writePartJson({
171
+ partDir: path.join(storage.part, "msg_f"),
172
+ fileName: "part_1.json",
173
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
174
+ })
175
+
176
+ writeMessageMeta({
177
+ messageDir,
178
+ messageId: "msg_g",
179
+ meta: { sessionID: mainSessionId, agent: "Sisyphus", time: { created: -1 } },
180
+ })
181
+ writePartJson({
182
+ partDir: path.join(storage.part, "msg_g"),
183
+ fileName: "part_1.json",
184
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
185
+ })
186
+
187
+ writeMessageMeta({
188
+ messageDir,
189
+ messageId: "msg_invalid",
190
+ meta: { sessionID: mainSessionId, agent: "Sisyphus", time: { created: "bad" } },
191
+ })
192
+ writePartJson({
193
+ partDir: path.join(storage.part, "msg_invalid"),
194
+ fileName: "part_1.json",
195
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
196
+ })
197
+
198
+ // #when
199
+ const result = deriveTimeSeriesActivity({
200
+ storage,
201
+ mainSessionId,
202
+ nowMs: 10_000,
203
+ windowMs: 10_000,
204
+ bucketMs: 2_000,
205
+ })
206
+
207
+ // #then
208
+ expect(result.anchorMs).toBe(10_000)
209
+ expect(result.buckets).toBe(5)
210
+
211
+ const overall = getSeries(result, "overall-main")
212
+ const sisyphus = getSeries(result, "agent:sisyphus")
213
+ const prometheus = getSeries(result, "agent:prometheus")
214
+ const atlas = getSeries(result, "agent:atlas")
215
+ const background = getSeries(result, "background-total")
216
+
217
+ expect(overall.values).toEqual([3, 1, 0, 0, 2])
218
+ expect(sisyphus.values).toEqual([2, 0, 0, 0, 1])
219
+ expect(prometheus.values).toEqual([1, 0, 0, 0, 0])
220
+ expect(atlas.values).toEqual([0, 1, 0, 0, 0])
221
+ expect(background.values).toEqual([0, 0, 0, 0, 0])
222
+ })
223
+
224
+ it("counts background task tool parts across child sessions", () => {
225
+ const storageRoot = mkStorageRoot()
226
+ const storage = getStorageRoots(storageRoot)
227
+ const mainSessionId = "ses_main"
228
+ const projectID = "proj"
229
+ const sessionDir = path.join(storage.session, projectID)
230
+ fs.mkdirSync(sessionDir, { recursive: true })
231
+
232
+ // #given
233
+ fs.writeFileSync(
234
+ path.join(sessionDir, "ses_child_a.json"),
235
+ JSON.stringify({
236
+ id: "ses_child_a",
237
+ projectID,
238
+ directory: "/tmp/project",
239
+ title: "Background: A",
240
+ parentID: mainSessionId,
241
+ time: { created: 1000, updated: 1000 },
242
+ }),
243
+ "utf8"
244
+ )
245
+ fs.writeFileSync(
246
+ path.join(sessionDir, "ses_child_b.json"),
247
+ JSON.stringify({
248
+ id: "ses_child_b",
249
+ projectID,
250
+ directory: "/tmp/project",
251
+ title: "Background: B",
252
+ parentID: mainSessionId,
253
+ time: { created: 2000, updated: 2000 },
254
+ }),
255
+ "utf8"
256
+ )
257
+
258
+ const childADir = path.join(storage.message, "ses_child_a")
259
+ writeMessageMeta({
260
+ messageDir: childADir,
261
+ messageId: "msg_child_a",
262
+ meta: { sessionID: "ses_child_a", time: { created: 4_000 } },
263
+ })
264
+ const childAParts = path.join(storage.part, "msg_child_a")
265
+ writePartJson({
266
+ partDir: childAParts,
267
+ fileName: "part_1.json",
268
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
269
+ })
270
+ writePartJson({
271
+ partDir: childAParts,
272
+ fileName: "part_2.json",
273
+ value: { type: "tool", tool: "grep", state: { status: "completed", input: {} } },
274
+ })
275
+ writePartJson({
276
+ partDir: childAParts,
277
+ fileName: "part_3.json",
278
+ value: { type: "text" },
279
+ })
280
+
281
+ const childBDir = path.join(storage.message, "ses_child_b")
282
+ writeMessageMeta({
283
+ messageDir: childBDir,
284
+ messageId: "msg_child_b",
285
+ meta: { sessionID: "ses_child_b", time: { created: 9_999 } },
286
+ })
287
+ const childBParts = path.join(storage.part, "msg_child_b")
288
+ writePartJson({
289
+ partDir: childBParts,
290
+ fileName: "part_1.json",
291
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
292
+ })
293
+ writeMalformedPart({ partDir: childBParts, fileName: "part_2.json", value: "{bad json" })
294
+
295
+ // #when
296
+ const result = deriveTimeSeriesActivity({
297
+ storage,
298
+ mainSessionId,
299
+ nowMs: 10_000,
300
+ windowMs: 10_000,
301
+ bucketMs: 2_000,
302
+ })
303
+
304
+ // #then
305
+ const background = getSeries(result, "background-total")
306
+ expect(background.values).toEqual([0, 0, 2, 0, 1])
307
+ })
308
+
309
+ it("attributes child session tool parts to overall and background series", () => {
310
+ const storageRoot = mkStorageRoot()
311
+ const storage = getStorageRoots(storageRoot)
312
+ const mainSessionId = "ses_main"
313
+ const projectID = "proj"
314
+ const sessionDir = path.join(storage.session, projectID)
315
+ fs.mkdirSync(sessionDir, { recursive: true })
316
+
317
+ // #given
318
+ writeMessageMeta({
319
+ messageDir: path.join(storage.message, mainSessionId),
320
+ messageId: "msg_main",
321
+ meta: { sessionID: mainSessionId, agent: "Atlas", time: { created: 1_000 } },
322
+ })
323
+ writePartJson({
324
+ partDir: path.join(storage.part, "msg_main"),
325
+ fileName: "part_1.json",
326
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
327
+ })
328
+
329
+ fs.writeFileSync(
330
+ path.join(sessionDir, "ses_child_a.json"),
331
+ JSON.stringify({
332
+ id: "ses_child_a",
333
+ projectID,
334
+ directory: "/tmp/project",
335
+ title: "Background: A",
336
+ parentID: mainSessionId,
337
+ time: { created: 1000, updated: 1000 },
338
+ }),
339
+ "utf8"
340
+ )
341
+
342
+ const childDir = path.join(storage.message, "ses_child_a")
343
+ writeMessageMeta({
344
+ messageDir: childDir,
345
+ messageId: "msg_child_a",
346
+ meta: { sessionID: "ses_child_a", agent: "sisyphus-junior", time: { created: 4_000 } },
347
+ })
348
+ const childParts = path.join(storage.part, "msg_child_a")
349
+ writePartJson({
350
+ partDir: childParts,
351
+ fileName: "part_1.json",
352
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
353
+ })
354
+ writePartJson({
355
+ partDir: childParts,
356
+ fileName: "part_2.json",
357
+ value: { type: "tool", tool: "grep", state: { status: "completed", input: {} } },
358
+ })
359
+ writePartJson({
360
+ partDir: childParts,
361
+ fileName: "part_3.json",
362
+ value: { type: "text" },
363
+ })
364
+
365
+ // #when
366
+ const result = deriveTimeSeriesActivity({
367
+ storage,
368
+ mainSessionId,
369
+ nowMs: 10_000,
370
+ windowMs: 10_000,
371
+ bucketMs: 2_000,
372
+ })
373
+
374
+ // #then
375
+ const overall = getSeries(result, "overall-main")
376
+ const sisyphus = getSeries(result, "agent:sisyphus")
377
+ const atlas = getSeries(result, "agent:atlas")
378
+ const background = getSeries(result, "background-total")
379
+
380
+ expect(overall.values).toEqual([1, 0, 2, 0, 0])
381
+ expect(sisyphus.values).toEqual([0, 0, 0, 0, 0])
382
+ expect(atlas.values).toEqual([1, 0, 0, 0, 0])
383
+ expect(background.values).toEqual([0, 0, 2, 0, 0])
384
+ })
385
+
386
+ it("does not attribute child session tool parts to the Sisyphus series", () => {
387
+ const storageRoot = mkStorageRoot()
388
+ const storage = getStorageRoots(storageRoot)
389
+ const mainSessionId = "ses_main"
390
+ const projectID = "proj"
391
+ const sessionDir = path.join(storage.session, projectID)
392
+ fs.mkdirSync(sessionDir, { recursive: true })
393
+
394
+ // #given
395
+ fs.writeFileSync(
396
+ path.join(sessionDir, "ses_child_a.json"),
397
+ JSON.stringify({
398
+ id: "ses_child_a",
399
+ projectID,
400
+ directory: "/tmp/project",
401
+ title: "Background: A",
402
+ parentID: mainSessionId,
403
+ time: { created: 1000, updated: 1000 },
404
+ }),
405
+ "utf8"
406
+ )
407
+
408
+ const childDir = path.join(storage.message, "ses_child_a")
409
+ writeMessageMeta({
410
+ messageDir: childDir,
411
+ messageId: "msg_child_a",
412
+ meta: { sessionID: "ses_child_a", agent: "sisyphus-junior", time: { created: 4_000 } },
413
+ })
414
+ const childParts = path.join(storage.part, "msg_child_a")
415
+ writePartJson({
416
+ partDir: childParts,
417
+ fileName: "part_1.json",
418
+ value: { type: "tool", tool: "read", state: { status: "completed", input: {} } },
419
+ })
420
+ writePartJson({
421
+ partDir: childParts,
422
+ fileName: "part_2.json",
423
+ value: { type: "tool", tool: "grep", state: { status: "completed", input: {} } },
424
+ })
425
+
426
+ // #when
427
+ const result = deriveTimeSeriesActivity({
428
+ storage,
429
+ mainSessionId,
430
+ nowMs: 10_000,
431
+ windowMs: 10_000,
432
+ bucketMs: 2_000,
433
+ })
434
+
435
+ // #then
436
+ const sisyphus = getSeries(result, "agent:sisyphus")
437
+ expect(sisyphus.values).toEqual([0, 0, 0, 0, 0])
438
+ })
439
+
440
+ it("does not attribute child session activity to Prometheus/Atlas series", () => {
441
+ const storageRoot = mkStorageRoot()
442
+ const storage = getStorageRoots(storageRoot)
443
+ const mainSessionId = "ses_main"
444
+ const childSessionId = "ses_child_a"
445
+
446
+ // #given
447
+ const projectID = "proj"
448
+ const sessionDir = path.join(storage.session, projectID)
449
+ fs.mkdirSync(sessionDir, { recursive: true })
450
+ fs.writeFileSync(
451
+ path.join(sessionDir, `${childSessionId}.json`),
452
+ JSON.stringify({
453
+ id: childSessionId,
454
+ projectID,
455
+ directory: "/tmp/project",
456
+ title: "Background: A",
457
+ parentID: mainSessionId,
458
+ time: { created: 1000, updated: 9000 },
459
+ }),
460
+ "utf8"
461
+ )
462
+
463
+ const childMessageDir = path.join(storage.message, childSessionId)
464
+ writeMessageMeta({
465
+ messageDir: childMessageDir,
466
+ messageId: "msg_child_a",
467
+ meta: { sessionID: childSessionId, agent: "Atlas", time: { created: 4_000 } },
468
+ })
469
+ const childParts = path.join(storage.part, "msg_child_a")
470
+ writePartJson({
471
+ partDir: childParts,
472
+ fileName: "part_1.json",
473
+ value: { type: "tool", tool: "grep", state: { status: "completed", input: {} } },
474
+ })
475
+
476
+ // #when
477
+ const result = deriveTimeSeriesActivity({
478
+ storage,
479
+ mainSessionId,
480
+ nowMs: 10_000,
481
+ windowMs: 10_000,
482
+ bucketMs: 1_000,
483
+ })
484
+
485
+ // #then
486
+ const prometheus = getSeries(result, "agent:prometheus")
487
+ const atlas = getSeries(result, "agent:atlas")
488
+ expect(prometheus.values.every((v) => v === 0)).toBe(true)
489
+ expect(atlas.values.every((v) => v === 0)).toBe(true)
490
+ })
491
+ })