oh-my-opencode-dashboard 0.0.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.
- package/README.md +100 -0
- package/dashboard-ui.png +0 -0
- package/dist/assets/index-D6OVzN1o.css +1 -0
- package/dist/assets/index-SEmwze_4.js +40 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +51 -0
- package/src/App.tsx +518 -0
- package/src/cli/dev.ts +139 -0
- package/src/cli/ports.test.ts +40 -0
- package/src/cli/ports.ts +43 -0
- package/src/ding-policy.test.ts +48 -0
- package/src/ding-policy.ts +39 -0
- package/src/ingest/background-tasks.test.ts +707 -0
- package/src/ingest/background-tasks.ts +317 -0
- package/src/ingest/boulder.test.ts +77 -0
- package/src/ingest/boulder.ts +71 -0
- package/src/ingest/paths.test.ts +82 -0
- package/src/ingest/paths.ts +76 -0
- package/src/ingest/session.test.ts +220 -0
- package/src/ingest/session.ts +283 -0
- package/src/main.tsx +10 -0
- package/src/server/api.test.ts +62 -0
- package/src/server/api.ts +16 -0
- package/src/server/build.ts +5 -0
- package/src/server/dashboard.test.ts +135 -0
- package/src/server/dashboard.ts +191 -0
- package/src/server/dev.ts +44 -0
- package/src/server/start.ts +93 -0
- package/src/sound.test.ts +55 -0
- package/src/sound.ts +89 -0
- package/src/styles.css +457 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,707 @@
|
|
|
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 { deriveBackgroundTasks } from "./background-tasks"
|
|
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
|
+
describe("deriveBackgroundTasks", () => {
|
|
17
|
+
it("extracts delegate_task background calls and correlates child sessions", () => {
|
|
18
|
+
const storageRoot = mkStorageRoot()
|
|
19
|
+
const storage = getStorageRoots(storageRoot)
|
|
20
|
+
const mainSessionId = "ses_main"
|
|
21
|
+
|
|
22
|
+
// Main session message + tool part
|
|
23
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
24
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
25
|
+
const messageID = "msg_1"
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(msgDir, `${messageID}.json`),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
id: messageID,
|
|
30
|
+
sessionID: mainSessionId,
|
|
31
|
+
role: "assistant",
|
|
32
|
+
time: { created: 1000 },
|
|
33
|
+
}),
|
|
34
|
+
"utf8"
|
|
35
|
+
)
|
|
36
|
+
const partDir = path.join(storage.part, messageID)
|
|
37
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(partDir, "part_1.json"),
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
id: "part_1",
|
|
42
|
+
sessionID: mainSessionId,
|
|
43
|
+
messageID,
|
|
44
|
+
type: "tool",
|
|
45
|
+
callID: "call_1",
|
|
46
|
+
tool: "delegate_task",
|
|
47
|
+
state: {
|
|
48
|
+
status: "completed",
|
|
49
|
+
input: {
|
|
50
|
+
run_in_background: true,
|
|
51
|
+
description: "Scan repo",
|
|
52
|
+
subagent_type: "explore",
|
|
53
|
+
prompt: "SECRET",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
"utf8"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Child session metadata that should be correlated
|
|
61
|
+
const projectID = "proj"
|
|
62
|
+
const sessDir = path.join(storage.session, projectID)
|
|
63
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(sessDir, "ses_child.json"),
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
id: "ses_child",
|
|
68
|
+
projectID,
|
|
69
|
+
directory: "/tmp/project",
|
|
70
|
+
title: "Background: Scan repo",
|
|
71
|
+
parentID: mainSessionId,
|
|
72
|
+
time: { created: 1500, updated: 1500 },
|
|
73
|
+
}),
|
|
74
|
+
"utf8"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Background session message with a tool call
|
|
78
|
+
const childMsgDir = path.join(storage.message, "ses_child")
|
|
79
|
+
fs.mkdirSync(childMsgDir, { recursive: true })
|
|
80
|
+
const childMsgId = "msg_child"
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
path.join(childMsgDir, `${childMsgId}.json`),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
id: childMsgId,
|
|
85
|
+
sessionID: "ses_child",
|
|
86
|
+
role: "assistant",
|
|
87
|
+
time: { created: 2000 },
|
|
88
|
+
}),
|
|
89
|
+
"utf8"
|
|
90
|
+
)
|
|
91
|
+
const childPartDir = path.join(storage.part, childMsgId)
|
|
92
|
+
fs.mkdirSync(childPartDir, { recursive: true })
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(childPartDir, "part_1.json"),
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
id: "part_1",
|
|
97
|
+
sessionID: "ses_child",
|
|
98
|
+
messageID: childMsgId,
|
|
99
|
+
type: "tool",
|
|
100
|
+
callID: "call_x",
|
|
101
|
+
tool: "grep",
|
|
102
|
+
state: { status: "completed", input: {} },
|
|
103
|
+
}),
|
|
104
|
+
"utf8"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId, nowMs: 3000 })
|
|
108
|
+
expect(rows.length).toBe(1)
|
|
109
|
+
expect(rows[0].description).toBe("Scan repo")
|
|
110
|
+
expect(rows[0].agent).toBe("explore")
|
|
111
|
+
expect(rows[0].sessionId).toBe("ses_child")
|
|
112
|
+
expect(rows[0].toolCalls).toBe(1)
|
|
113
|
+
expect(rows[0].lastTool).toBe("grep")
|
|
114
|
+
expect(rows[0].timeline).toBe("1970-01-01T00:00:01Z: 2s")
|
|
115
|
+
|
|
116
|
+
const completed = deriveBackgroundTasks({ storage, mainSessionId, nowMs: 20_000 })
|
|
117
|
+
expect(completed.length).toBe(1)
|
|
118
|
+
expect(completed[0].status).toBe("completed")
|
|
119
|
+
expect(completed[0].timeline).toBe("1970-01-01T00:00:01Z: 1s")
|
|
120
|
+
|
|
121
|
+
// Ensure no sensitive keys leak
|
|
122
|
+
expect((rows[0] as unknown as Record<string, unknown>).prompt).toBeUndefined()
|
|
123
|
+
expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
|
|
124
|
+
expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("selects deterministic background session when multiple candidates have identical created timestamps", () => {
|
|
128
|
+
const storageRoot = mkStorageRoot()
|
|
129
|
+
const storage = getStorageRoots(storageRoot)
|
|
130
|
+
const mainSessionId = "ses_main"
|
|
131
|
+
|
|
132
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
133
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
134
|
+
const messageID = "msg_1"
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(msgDir, `${messageID}.json`),
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
id: messageID,
|
|
139
|
+
sessionID: mainSessionId,
|
|
140
|
+
role: "assistant",
|
|
141
|
+
time: { created: 1000 },
|
|
142
|
+
}),
|
|
143
|
+
"utf8"
|
|
144
|
+
)
|
|
145
|
+
const partDir = path.join(storage.part, messageID)
|
|
146
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(partDir, "part_tie.json"),
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
id: "part_tie",
|
|
151
|
+
sessionID: mainSessionId,
|
|
152
|
+
messageID,
|
|
153
|
+
type: "tool",
|
|
154
|
+
callID: "call_tie",
|
|
155
|
+
tool: "delegate_task",
|
|
156
|
+
state: {
|
|
157
|
+
status: "completed",
|
|
158
|
+
input: {
|
|
159
|
+
run_in_background: true,
|
|
160
|
+
description: "Tie-break test",
|
|
161
|
+
subagent_type: "explore",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
"utf8"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const projectID = "proj"
|
|
169
|
+
const sessDir = path.join(storage.session, projectID)
|
|
170
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
171
|
+
|
|
172
|
+
const sharedTimestamp = 1500
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(sessDir, "ses_zzz.json"),
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
id: "ses_zzz",
|
|
177
|
+
projectID,
|
|
178
|
+
directory: "/tmp/project",
|
|
179
|
+
title: "Background: Tie-break test",
|
|
180
|
+
parentID: mainSessionId,
|
|
181
|
+
time: { created: sharedTimestamp, updated: sharedTimestamp },
|
|
182
|
+
}),
|
|
183
|
+
"utf8"
|
|
184
|
+
)
|
|
185
|
+
fs.writeFileSync(
|
|
186
|
+
path.join(sessDir, "ses_aaa.json"),
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
id: "ses_aaa",
|
|
189
|
+
projectID,
|
|
190
|
+
directory: "/tmp/project",
|
|
191
|
+
title: "Background: Tie-break test",
|
|
192
|
+
parentID: mainSessionId,
|
|
193
|
+
time: { created: sharedTimestamp, updated: sharedTimestamp },
|
|
194
|
+
}),
|
|
195
|
+
"utf8"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
199
|
+
expect(rows.length).toBe(1)
|
|
200
|
+
expect(rows[0].sessionId).toBe("ses_aaa")
|
|
201
|
+
expect(rows[0].description).toBe("Tie-break test")
|
|
202
|
+
expect(rows[0].agent).toBe("explore")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("includes sync delegate_task rows when run_in_background is false", () => {
|
|
206
|
+
const storageRoot = mkStorageRoot()
|
|
207
|
+
const storage = getStorageRoots(storageRoot)
|
|
208
|
+
const mainSessionId = "ses_main"
|
|
209
|
+
|
|
210
|
+
// Main session message + sync tool part
|
|
211
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
212
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
213
|
+
const messageID = "msg_1"
|
|
214
|
+
fs.writeFileSync(
|
|
215
|
+
path.join(msgDir, `${messageID}.json`),
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
id: messageID,
|
|
218
|
+
sessionID: mainSessionId,
|
|
219
|
+
role: "assistant",
|
|
220
|
+
time: { created: 1000 },
|
|
221
|
+
}),
|
|
222
|
+
"utf8"
|
|
223
|
+
)
|
|
224
|
+
const partDir = path.join(storage.part, messageID)
|
|
225
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(partDir, "part_sync.json"),
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
id: "part_sync",
|
|
230
|
+
sessionID: mainSessionId,
|
|
231
|
+
messageID,
|
|
232
|
+
type: "tool",
|
|
233
|
+
callID: "call_sync",
|
|
234
|
+
tool: "delegate_task",
|
|
235
|
+
state: {
|
|
236
|
+
status: "completed",
|
|
237
|
+
input: {
|
|
238
|
+
run_in_background: false,
|
|
239
|
+
description: "Quick analysis",
|
|
240
|
+
category: "quick",
|
|
241
|
+
prompt: "SHOULD NOT APPEAR",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
"utf8"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
249
|
+
expect(rows.length).toBe(1)
|
|
250
|
+
expect(rows[0].description).toBe("Quick analysis")
|
|
251
|
+
expect(rows[0].agent).toBe("sisyphus-junior (quick)")
|
|
252
|
+
expect(rows[0].sessionId).toBe(null) // No background session for sync tasks
|
|
253
|
+
expect(rows[0].toolCalls).toBe(null) // No background session stats for sync tasks
|
|
254
|
+
expect(rows[0].lastTool).toBe(null)
|
|
255
|
+
expect(rows[0].status).toBe("queued") // Should show queued for unlinked sync tasks
|
|
256
|
+
|
|
257
|
+
// Ensure no sensitive keys leak
|
|
258
|
+
expect((rows[0] as unknown as Record<string, unknown>).prompt).toBeUndefined()
|
|
259
|
+
expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
|
|
260
|
+
expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it("selects deterministic task session when multiple candidates have identical created timestamps", () => {
|
|
264
|
+
const storageRoot = mkStorageRoot()
|
|
265
|
+
const storage = getStorageRoots(storageRoot)
|
|
266
|
+
const mainSessionId = "ses_main"
|
|
267
|
+
|
|
268
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
269
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
270
|
+
const messageID = "msg_1"
|
|
271
|
+
fs.writeFileSync(
|
|
272
|
+
path.join(msgDir, `${messageID}.json`),
|
|
273
|
+
JSON.stringify({
|
|
274
|
+
id: messageID,
|
|
275
|
+
sessionID: mainSessionId,
|
|
276
|
+
role: "assistant",
|
|
277
|
+
time: { created: 1000 },
|
|
278
|
+
}),
|
|
279
|
+
"utf8"
|
|
280
|
+
)
|
|
281
|
+
const partDir = path.join(storage.part, messageID)
|
|
282
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
283
|
+
fs.writeFileSync(
|
|
284
|
+
path.join(partDir, "part_tie_task.json"),
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
id: "part_tie_task",
|
|
287
|
+
sessionID: mainSessionId,
|
|
288
|
+
messageID,
|
|
289
|
+
type: "tool",
|
|
290
|
+
callID: "call_tie_task",
|
|
291
|
+
tool: "delegate_task",
|
|
292
|
+
state: {
|
|
293
|
+
status: "completed",
|
|
294
|
+
input: {
|
|
295
|
+
run_in_background: false,
|
|
296
|
+
description: "Task tie-break test",
|
|
297
|
+
category: "quick",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
"utf8"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const projectID = "proj"
|
|
305
|
+
const sessDir = path.join(storage.session, projectID)
|
|
306
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
307
|
+
|
|
308
|
+
const sharedTimestamp = 1500
|
|
309
|
+
fs.writeFileSync(
|
|
310
|
+
path.join(sessDir, "ses_task_zzz.json"),
|
|
311
|
+
JSON.stringify({
|
|
312
|
+
id: "ses_task_zzz",
|
|
313
|
+
projectID,
|
|
314
|
+
directory: "/tmp/project",
|
|
315
|
+
title: "Task: Task tie-break test",
|
|
316
|
+
parentID: mainSessionId,
|
|
317
|
+
time: { created: sharedTimestamp, updated: sharedTimestamp },
|
|
318
|
+
}),
|
|
319
|
+
"utf8"
|
|
320
|
+
)
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(sessDir, "ses_task_aaa.json"),
|
|
323
|
+
JSON.stringify({
|
|
324
|
+
id: "ses_task_aaa",
|
|
325
|
+
projectID,
|
|
326
|
+
directory: "/tmp/project",
|
|
327
|
+
title: "Task: Task tie-break test",
|
|
328
|
+
parentID: mainSessionId,
|
|
329
|
+
time: { created: sharedTimestamp, updated: sharedTimestamp },
|
|
330
|
+
}),
|
|
331
|
+
"utf8"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
335
|
+
expect(rows.length).toBe(1)
|
|
336
|
+
expect(rows[0].sessionId).toBe("ses_task_aaa")
|
|
337
|
+
expect(rows[0].description).toBe("Task tie-break test")
|
|
338
|
+
expect(rows[0].agent).toBe("sisyphus-junior (quick)")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it("links sync delegate_task rows to Task sessions when available", () => {
|
|
342
|
+
const storageRoot = mkStorageRoot()
|
|
343
|
+
const storage = getStorageRoots(storageRoot)
|
|
344
|
+
const mainSessionId = "ses_main"
|
|
345
|
+
|
|
346
|
+
// Main session message + sync tool part
|
|
347
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
348
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
349
|
+
const messageID = "msg_1"
|
|
350
|
+
fs.writeFileSync(
|
|
351
|
+
path.join(msgDir, `${messageID}.json`),
|
|
352
|
+
JSON.stringify({
|
|
353
|
+
id: messageID,
|
|
354
|
+
sessionID: mainSessionId,
|
|
355
|
+
role: "assistant",
|
|
356
|
+
time: { created: 1000 },
|
|
357
|
+
}),
|
|
358
|
+
"utf8"
|
|
359
|
+
)
|
|
360
|
+
const partDir = path.join(storage.part, messageID)
|
|
361
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
362
|
+
fs.writeFileSync(
|
|
363
|
+
path.join(partDir, "part_sync.json"),
|
|
364
|
+
JSON.stringify({
|
|
365
|
+
id: "part_sync",
|
|
366
|
+
sessionID: mainSessionId,
|
|
367
|
+
messageID,
|
|
368
|
+
type: "tool",
|
|
369
|
+
callID: "call_sync",
|
|
370
|
+
tool: "delegate_task",
|
|
371
|
+
state: {
|
|
372
|
+
status: "completed",
|
|
373
|
+
input: {
|
|
374
|
+
run_in_background: false,
|
|
375
|
+
description: "Quick analysis",
|
|
376
|
+
category: "quick",
|
|
377
|
+
prompt: "SHOULD NOT APPEAR",
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
}),
|
|
381
|
+
"utf8"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
// Child session metadata with Task: title that should be correlated
|
|
385
|
+
const projectID = "proj"
|
|
386
|
+
const sessDir = path.join(storage.session, projectID)
|
|
387
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
388
|
+
fs.writeFileSync(
|
|
389
|
+
path.join(sessDir, "ses_task.json"),
|
|
390
|
+
JSON.stringify({
|
|
391
|
+
id: "ses_task",
|
|
392
|
+
projectID,
|
|
393
|
+
directory: "/tmp/project",
|
|
394
|
+
title: "Task: Quick analysis",
|
|
395
|
+
parentID: mainSessionId,
|
|
396
|
+
time: { created: 1050, updated: 1050 },
|
|
397
|
+
}),
|
|
398
|
+
"utf8"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// Task session message with a tool call
|
|
402
|
+
const taskMsgDir = path.join(storage.message, "ses_task")
|
|
403
|
+
fs.mkdirSync(taskMsgDir, { recursive: true })
|
|
404
|
+
const taskMsgId = "msg_task"
|
|
405
|
+
fs.writeFileSync(
|
|
406
|
+
path.join(taskMsgDir, `${taskMsgId}.json`),
|
|
407
|
+
JSON.stringify({
|
|
408
|
+
id: taskMsgId,
|
|
409
|
+
sessionID: "ses_task",
|
|
410
|
+
role: "assistant",
|
|
411
|
+
time: { created: 1100 },
|
|
412
|
+
}),
|
|
413
|
+
"utf8"
|
|
414
|
+
)
|
|
415
|
+
const taskPartDir = path.join(storage.part, taskMsgId)
|
|
416
|
+
fs.mkdirSync(taskPartDir, { recursive: true })
|
|
417
|
+
fs.writeFileSync(
|
|
418
|
+
path.join(taskPartDir, "part_1.json"),
|
|
419
|
+
JSON.stringify({
|
|
420
|
+
id: "part_1",
|
|
421
|
+
sessionID: "ses_task",
|
|
422
|
+
messageID: taskMsgId,
|
|
423
|
+
type: "tool",
|
|
424
|
+
callID: "call_x",
|
|
425
|
+
tool: "read",
|
|
426
|
+
state: { status: "completed", input: {} },
|
|
427
|
+
}),
|
|
428
|
+
"utf8"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
432
|
+
expect(rows.length).toBe(1)
|
|
433
|
+
expect(rows[0].description).toBe("Quick analysis")
|
|
434
|
+
expect(rows[0].agent).toBe("sisyphus-junior (quick)")
|
|
435
|
+
expect(rows[0].sessionId).toBe("ses_task") // Should be linked to Task session
|
|
436
|
+
expect(rows[0].toolCalls).toBe(1)
|
|
437
|
+
expect(rows[0].lastTool).toBe("read")
|
|
438
|
+
expect(rows[0].status).toBe("completed") // Should be completed since Task session has tool calls
|
|
439
|
+
|
|
440
|
+
// Ensure no sensitive keys leak
|
|
441
|
+
expect((rows[0] as unknown as Record<string, unknown>).prompt).toBeUndefined()
|
|
442
|
+
expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
|
|
443
|
+
expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it("links sync delegate_task rows to resumed session when resume is specified", () => {
|
|
447
|
+
const storageRoot = mkStorageRoot()
|
|
448
|
+
const storage = getStorageRoots(storageRoot)
|
|
449
|
+
const mainSessionId = "ses_main"
|
|
450
|
+
const resumedSessionId = "ses_resumed"
|
|
451
|
+
|
|
452
|
+
// Main session message + sync tool part with resume
|
|
453
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
454
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
455
|
+
const messageID = "msg_1"
|
|
456
|
+
fs.writeFileSync(
|
|
457
|
+
path.join(msgDir, `${messageID}.json`),
|
|
458
|
+
JSON.stringify({
|
|
459
|
+
id: messageID,
|
|
460
|
+
sessionID: mainSessionId,
|
|
461
|
+
role: "assistant",
|
|
462
|
+
time: { created: 1000 },
|
|
463
|
+
}),
|
|
464
|
+
"utf8"
|
|
465
|
+
)
|
|
466
|
+
const partDir = path.join(storage.part, messageID)
|
|
467
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
468
|
+
fs.writeFileSync(
|
|
469
|
+
path.join(partDir, "part_resume.json"),
|
|
470
|
+
JSON.stringify({
|
|
471
|
+
id: "part_resume",
|
|
472
|
+
sessionID: mainSessionId,
|
|
473
|
+
messageID,
|
|
474
|
+
type: "tool",
|
|
475
|
+
callID: "call_resume",
|
|
476
|
+
tool: "delegate_task",
|
|
477
|
+
state: {
|
|
478
|
+
status: "completed",
|
|
479
|
+
input: {
|
|
480
|
+
run_in_background: false,
|
|
481
|
+
description: "Resume work",
|
|
482
|
+
category: "quick",
|
|
483
|
+
resume: resumedSessionId,
|
|
484
|
+
prompt: "SHOULD NOT APPEAR",
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
488
|
+
"utf8"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
// Resumed session message + tool call
|
|
492
|
+
const resumedMsgDir = path.join(storage.message, resumedSessionId)
|
|
493
|
+
fs.mkdirSync(resumedMsgDir, { recursive: true })
|
|
494
|
+
const resumedMsgId = "msg_resumed"
|
|
495
|
+
fs.writeFileSync(
|
|
496
|
+
path.join(resumedMsgDir, `${resumedMsgId}.json`),
|
|
497
|
+
JSON.stringify({
|
|
498
|
+
id: resumedMsgId,
|
|
499
|
+
sessionID: resumedSessionId,
|
|
500
|
+
role: "assistant",
|
|
501
|
+
time: { created: 500 },
|
|
502
|
+
}),
|
|
503
|
+
"utf8"
|
|
504
|
+
)
|
|
505
|
+
const resumedPartDir = path.join(storage.part, resumedMsgId)
|
|
506
|
+
fs.mkdirSync(resumedPartDir, { recursive: true })
|
|
507
|
+
fs.writeFileSync(
|
|
508
|
+
path.join(resumedPartDir, "part_1.json"),
|
|
509
|
+
JSON.stringify({
|
|
510
|
+
id: "part_1",
|
|
511
|
+
sessionID: resumedSessionId,
|
|
512
|
+
messageID: resumedMsgId,
|
|
513
|
+
type: "tool",
|
|
514
|
+
callID: "call_grep",
|
|
515
|
+
tool: "grep",
|
|
516
|
+
state: { status: "completed", input: {} },
|
|
517
|
+
}),
|
|
518
|
+
"utf8"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
522
|
+
expect(rows.length).toBe(1)
|
|
523
|
+
expect(rows[0].description).toBe("Resume work")
|
|
524
|
+
expect(rows[0].agent).toBe("sisyphus-junior (quick)")
|
|
525
|
+
expect(rows[0].sessionId).toBe(resumedSessionId) // Should be linked to resumed session
|
|
526
|
+
expect(rows[0].toolCalls).toBe(1)
|
|
527
|
+
expect(rows[0].lastTool).toBe("grep")
|
|
528
|
+
expect(rows[0].status).toBe("completed") // Should be completed since resumed session has tool calls
|
|
529
|
+
|
|
530
|
+
// Ensure no sensitive keys leak
|
|
531
|
+
expect((rows[0] as unknown as Record<string, unknown>).prompt).toBeUndefined()
|
|
532
|
+
expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
|
|
533
|
+
expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it("falls back to title-based matching when resume session does not exist", () => {
|
|
537
|
+
const storageRoot = mkStorageRoot()
|
|
538
|
+
const storage = getStorageRoots(storageRoot)
|
|
539
|
+
const mainSessionId = "ses_main"
|
|
540
|
+
const nonExistentResumeId = "ses_nonexistent"
|
|
541
|
+
|
|
542
|
+
// Main session message + sync tool part with non-existent resume
|
|
543
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
544
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
545
|
+
const messageID = "msg_1"
|
|
546
|
+
fs.writeFileSync(
|
|
547
|
+
path.join(msgDir, `${messageID}.json`),
|
|
548
|
+
JSON.stringify({
|
|
549
|
+
id: messageID,
|
|
550
|
+
sessionID: mainSessionId,
|
|
551
|
+
role: "assistant",
|
|
552
|
+
time: { created: 1000 },
|
|
553
|
+
}),
|
|
554
|
+
"utf8"
|
|
555
|
+
)
|
|
556
|
+
const partDir = path.join(storage.part, messageID)
|
|
557
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
558
|
+
fs.writeFileSync(
|
|
559
|
+
path.join(partDir, "part_fallback.json"),
|
|
560
|
+
JSON.stringify({
|
|
561
|
+
id: "part_fallback",
|
|
562
|
+
sessionID: mainSessionId,
|
|
563
|
+
messageID,
|
|
564
|
+
type: "tool",
|
|
565
|
+
callID: "call_fallback",
|
|
566
|
+
tool: "delegate_task",
|
|
567
|
+
state: {
|
|
568
|
+
status: "completed",
|
|
569
|
+
input: {
|
|
570
|
+
run_in_background: false,
|
|
571
|
+
description: "Fallback task",
|
|
572
|
+
category: "quick",
|
|
573
|
+
resume: nonExistentResumeId,
|
|
574
|
+
prompt: "SHOULD NOT APPEAR",
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
}),
|
|
578
|
+
"utf8"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
// Child session metadata with Task: title that should be used as fallback
|
|
582
|
+
const projectID = "proj"
|
|
583
|
+
const sessDir = path.join(storage.session, projectID)
|
|
584
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
585
|
+
fs.writeFileSync(
|
|
586
|
+
path.join(sessDir, "ses_task.json"),
|
|
587
|
+
JSON.stringify({
|
|
588
|
+
id: "ses_task",
|
|
589
|
+
projectID,
|
|
590
|
+
directory: "/tmp/project",
|
|
591
|
+
title: "Task: Fallback task",
|
|
592
|
+
parentID: mainSessionId,
|
|
593
|
+
time: { created: 1050, updated: 1050 },
|
|
594
|
+
}),
|
|
595
|
+
"utf8"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
599
|
+
expect(rows.length).toBe(1)
|
|
600
|
+
expect(rows[0].description).toBe("Fallback task")
|
|
601
|
+
expect(rows[0].sessionId).toBe("ses_task") // Should fallback to Task session
|
|
602
|
+
expect(rows[0].toolCalls).toBe(0) // No tool calls in fallback session
|
|
603
|
+
expect(rows[0].lastTool).toBe(null)
|
|
604
|
+
expect(rows[0].status).toBe("unknown") // Should be unknown since session exists but no tool calls
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
it("links sync delegate_task rows to Background sessions when forced-to-background but waited", () => {
|
|
608
|
+
const storageRoot = mkStorageRoot()
|
|
609
|
+
const storage = getStorageRoots(storageRoot)
|
|
610
|
+
const mainSessionId = "ses_main"
|
|
611
|
+
|
|
612
|
+
const msgDir = path.join(storage.message, mainSessionId)
|
|
613
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
614
|
+
const messageID = "msg_1"
|
|
615
|
+
fs.writeFileSync(
|
|
616
|
+
path.join(msgDir, `${messageID}.json`),
|
|
617
|
+
JSON.stringify({
|
|
618
|
+
id: messageID,
|
|
619
|
+
sessionID: mainSessionId,
|
|
620
|
+
role: "assistant",
|
|
621
|
+
time: { created: 1000 },
|
|
622
|
+
}),
|
|
623
|
+
"utf8"
|
|
624
|
+
)
|
|
625
|
+
const partDir = path.join(storage.part, messageID)
|
|
626
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
627
|
+
fs.writeFileSync(
|
|
628
|
+
path.join(partDir, "part_forced.json"),
|
|
629
|
+
JSON.stringify({
|
|
630
|
+
id: "part_forced",
|
|
631
|
+
sessionID: mainSessionId,
|
|
632
|
+
messageID,
|
|
633
|
+
type: "tool",
|
|
634
|
+
callID: "call_forced",
|
|
635
|
+
tool: "delegate_task",
|
|
636
|
+
state: {
|
|
637
|
+
status: "completed",
|
|
638
|
+
input: {
|
|
639
|
+
run_in_background: false,
|
|
640
|
+
description: "Forced background task",
|
|
641
|
+
category: "quick",
|
|
642
|
+
prompt: "SHOULD NOT APPEAR",
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
"utf8"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
const projectID = "proj"
|
|
650
|
+
const sessDir = path.join(storage.session, projectID)
|
|
651
|
+
fs.mkdirSync(sessDir, { recursive: true })
|
|
652
|
+
fs.writeFileSync(
|
|
653
|
+
path.join(sessDir, "ses_background.json"),
|
|
654
|
+
JSON.stringify({
|
|
655
|
+
id: "ses_background",
|
|
656
|
+
projectID,
|
|
657
|
+
directory: "/tmp/project",
|
|
658
|
+
title: "Background: Forced background task",
|
|
659
|
+
parentID: mainSessionId,
|
|
660
|
+
time: { created: 1050, updated: 1050 },
|
|
661
|
+
}),
|
|
662
|
+
"utf8"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
const backgroundMsgDir = path.join(storage.message, "ses_background")
|
|
666
|
+
fs.mkdirSync(backgroundMsgDir, { recursive: true })
|
|
667
|
+
const backgroundMsgId = "msg_background"
|
|
668
|
+
fs.writeFileSync(
|
|
669
|
+
path.join(backgroundMsgDir, `${backgroundMsgId}.json`),
|
|
670
|
+
JSON.stringify({
|
|
671
|
+
id: backgroundMsgId,
|
|
672
|
+
sessionID: "ses_background",
|
|
673
|
+
role: "assistant",
|
|
674
|
+
time: { created: 1100 },
|
|
675
|
+
}),
|
|
676
|
+
"utf8"
|
|
677
|
+
)
|
|
678
|
+
const backgroundPartDir = path.join(storage.part, backgroundMsgId)
|
|
679
|
+
fs.mkdirSync(backgroundPartDir, { recursive: true })
|
|
680
|
+
fs.writeFileSync(
|
|
681
|
+
path.join(backgroundPartDir, "part_1.json"),
|
|
682
|
+
JSON.stringify({
|
|
683
|
+
id: "part_1",
|
|
684
|
+
sessionID: "ses_background",
|
|
685
|
+
messageID: backgroundMsgId,
|
|
686
|
+
type: "tool",
|
|
687
|
+
callID: "call_x",
|
|
688
|
+
tool: "bash",
|
|
689
|
+
state: { status: "completed", input: {} },
|
|
690
|
+
}),
|
|
691
|
+
"utf8"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
const rows = deriveBackgroundTasks({ storage, mainSessionId })
|
|
695
|
+
expect(rows.length).toBe(1)
|
|
696
|
+
expect(rows[0].description).toBe("Forced background task")
|
|
697
|
+
expect(rows[0].agent).toBe("sisyphus-junior (quick)")
|
|
698
|
+
expect(rows[0].sessionId).toBe("ses_background")
|
|
699
|
+
expect(rows[0].toolCalls).toBe(1)
|
|
700
|
+
expect(rows[0].lastTool).toBe("bash")
|
|
701
|
+
expect(rows[0].status).toBe("completed")
|
|
702
|
+
|
|
703
|
+
expect((rows[0] as unknown as Record<string, unknown>).prompt).toBeUndefined()
|
|
704
|
+
expect((rows[0] as unknown as Record<string, unknown>).input).toBeUndefined()
|
|
705
|
+
expect((rows[0] as unknown as Record<string, unknown>).state).toBeUndefined()
|
|
706
|
+
})
|
|
707
|
+
})
|