@kkelly-offical/kkcode 0.1.3 → 0.1.6
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 +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +41 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +1 -1
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/engine.mjs +227 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1081 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +884 -1462
- package/src/session/project-context.mjs +30 -0
- package/src/session/store.mjs +510 -503
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
package/src/session/store.mjs
CHANGED
|
@@ -1,503 +1,510 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { access, readdir, unlink, rm } from "node:fs/promises"
|
|
4
|
-
import {
|
|
5
|
-
ensureUserRoot,
|
|
6
|
-
ensureSessionShardRoot,
|
|
7
|
-
sessionIndexPath,
|
|
8
|
-
sessionDataPath,
|
|
9
|
-
legacySessionStorePath,
|
|
10
|
-
sessionShardRootPath,
|
|
11
|
-
sessionCheckpointRootPath
|
|
12
|
-
} from "../storage/paths.mjs"
|
|
13
|
-
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
14
|
-
|
|
15
|
-
function now() {
|
|
16
|
-
return Date.now()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function defaultIndex() {
|
|
20
|
-
return {
|
|
21
|
-
version: 2,
|
|
22
|
-
updatedAt: now(),
|
|
23
|
-
sessions: {}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function defaultSessionData() {
|
|
28
|
-
return {
|
|
29
|
-
messages: [],
|
|
30
|
-
parts: []
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function newMessage(role, content, extra = {}) {
|
|
35
|
-
return {
|
|
36
|
-
id: `msg_${randomUUID().slice(0, 12)}`,
|
|
37
|
-
role,
|
|
38
|
-
content,
|
|
39
|
-
createdAt: now(),
|
|
40
|
-
...extra
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function newPart(type, payload = {}) {
|
|
45
|
-
return {
|
|
46
|
-
id: `part_${randomUUID().slice(0, 12)}`,
|
|
47
|
-
type,
|
|
48
|
-
createdAt: now(),
|
|
49
|
-
...payload
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function normalizeSessionData(raw) {
|
|
54
|
-
if (!raw || typeof raw !== "object") return defaultSessionData()
|
|
55
|
-
return {
|
|
56
|
-
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
|
57
|
-
parts: Array.isArray(raw.parts) ? raw.parts : []
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function exists(file) {
|
|
62
|
-
try {
|
|
63
|
-
await access(file)
|
|
64
|
-
return true
|
|
65
|
-
} catch {
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const state = {
|
|
71
|
-
loaded: false,
|
|
72
|
-
index: defaultIndex(),
|
|
73
|
-
sessionCache: new Map(),
|
|
74
|
-
dirtyIndex: false,
|
|
75
|
-
dirtySessions: new Set(),
|
|
76
|
-
flushTimer: null,
|
|
77
|
-
options: {
|
|
78
|
-
sessionShardEnabled: true,
|
|
79
|
-
flushIntervalMs: 1000
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
await
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
await
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
await
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
session
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { access, readdir, unlink, rm } from "node:fs/promises"
|
|
4
|
+
import {
|
|
5
|
+
ensureUserRoot,
|
|
6
|
+
ensureSessionShardRoot,
|
|
7
|
+
sessionIndexPath,
|
|
8
|
+
sessionDataPath,
|
|
9
|
+
legacySessionStorePath,
|
|
10
|
+
sessionShardRootPath,
|
|
11
|
+
sessionCheckpointRootPath
|
|
12
|
+
} from "../storage/paths.mjs"
|
|
13
|
+
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
14
|
+
|
|
15
|
+
function now() {
|
|
16
|
+
return Date.now()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultIndex() {
|
|
20
|
+
return {
|
|
21
|
+
version: 2,
|
|
22
|
+
updatedAt: now(),
|
|
23
|
+
sessions: {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultSessionData() {
|
|
28
|
+
return {
|
|
29
|
+
messages: [],
|
|
30
|
+
parts: []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function newMessage(role, content, extra = {}) {
|
|
35
|
+
return {
|
|
36
|
+
id: `msg_${randomUUID().slice(0, 12)}`,
|
|
37
|
+
role,
|
|
38
|
+
content,
|
|
39
|
+
createdAt: now(),
|
|
40
|
+
...extra
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function newPart(type, payload = {}) {
|
|
45
|
+
return {
|
|
46
|
+
id: `part_${randomUUID().slice(0, 12)}`,
|
|
47
|
+
type,
|
|
48
|
+
createdAt: now(),
|
|
49
|
+
...payload
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSessionData(raw) {
|
|
54
|
+
if (!raw || typeof raw !== "object") return defaultSessionData()
|
|
55
|
+
return {
|
|
56
|
+
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
|
57
|
+
parts: Array.isArray(raw.parts) ? raw.parts : []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function exists(file) {
|
|
62
|
+
try {
|
|
63
|
+
await access(file)
|
|
64
|
+
return true
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const state = {
|
|
71
|
+
loaded: false,
|
|
72
|
+
index: defaultIndex(),
|
|
73
|
+
sessionCache: new Map(),
|
|
74
|
+
dirtyIndex: false,
|
|
75
|
+
dirtySessions: new Set(),
|
|
76
|
+
flushTimer: null,
|
|
77
|
+
options: {
|
|
78
|
+
sessionShardEnabled: true,
|
|
79
|
+
flushIntervalMs: 1000
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const LOCK_TIMEOUT_MS = 30000
|
|
84
|
+
|
|
85
|
+
let lock = Promise.resolve()
|
|
86
|
+
function withLock(fn) {
|
|
87
|
+
const run = lock.then(fn, fn)
|
|
88
|
+
lock = run.then(
|
|
89
|
+
() => undefined,
|
|
90
|
+
() => undefined
|
|
91
|
+
)
|
|
92
|
+
return Promise.race([
|
|
93
|
+
run,
|
|
94
|
+
new Promise((_, reject) => {
|
|
95
|
+
setTimeout(() => reject(new Error("[store] withLock timeout after 30s")), LOCK_TIMEOUT_MS)
|
|
96
|
+
})
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scheduleFlush() {
|
|
101
|
+
if (state.options.flushIntervalMs <= 0) return
|
|
102
|
+
if (state.flushTimer) return
|
|
103
|
+
state.flushTimer = setTimeout(() => {
|
|
104
|
+
state.flushTimer = null
|
|
105
|
+
flushNow().catch((err) => {
|
|
106
|
+
console.error("[store] flush failed:", err?.message || err)
|
|
107
|
+
})
|
|
108
|
+
}, state.options.flushIntervalMs)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function markDirty(sessionId = null) {
|
|
112
|
+
state.dirtyIndex = true
|
|
113
|
+
if (sessionId) state.dirtySessions.add(sessionId)
|
|
114
|
+
if (state.options.flushIntervalMs <= 0) return
|
|
115
|
+
scheduleFlush()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function flushUnsafe() {
|
|
119
|
+
if (!state.loaded) return
|
|
120
|
+
await ensureUserRoot()
|
|
121
|
+
await ensureSessionShardRoot()
|
|
122
|
+
|
|
123
|
+
for (const sessionId of [...state.dirtySessions]) {
|
|
124
|
+
const data = state.sessionCache.get(sessionId) || defaultSessionData()
|
|
125
|
+
await writeJson(sessionDataPath(sessionId), data)
|
|
126
|
+
state.dirtySessions.delete(sessionId)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (state.dirtyIndex) {
|
|
130
|
+
state.index.updatedAt = now()
|
|
131
|
+
await writeJson(sessionIndexPath(), state.index)
|
|
132
|
+
state.dirtyIndex = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function flushNow() {
|
|
137
|
+
return withLock(async () => {
|
|
138
|
+
await flushUnsafe()
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function loadSessionDataUnsafe(sessionId) {
|
|
143
|
+
if (state.sessionCache.has(sessionId)) {
|
|
144
|
+
return state.sessionCache.get(sessionId)
|
|
145
|
+
}
|
|
146
|
+
const data = normalizeSessionData(await readJson(sessionDataPath(sessionId), defaultSessionData()))
|
|
147
|
+
state.sessionCache.set(sessionId, data)
|
|
148
|
+
return data
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function migrateLegacyStoreIfNeededUnsafe() {
|
|
152
|
+
const indexFile = sessionIndexPath()
|
|
153
|
+
if (await exists(indexFile)) {
|
|
154
|
+
state.index = await readJson(indexFile, defaultIndex())
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const legacy = await readJson(legacySessionStorePath(), null)
|
|
159
|
+
if (!legacy || typeof legacy !== "object" || !legacy.sessions || typeof legacy.sessions !== "object") {
|
|
160
|
+
state.index = defaultIndex()
|
|
161
|
+
await writeJson(indexFile, state.index)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const next = defaultIndex()
|
|
166
|
+
for (const [sessionId, session] of Object.entries(legacy.sessions || {})) {
|
|
167
|
+
next.sessions[sessionId] = {
|
|
168
|
+
...session
|
|
169
|
+
}
|
|
170
|
+
const data = normalizeSessionData({
|
|
171
|
+
messages: legacy.messages?.[sessionId] || [],
|
|
172
|
+
parts: legacy.parts?.[sessionId] || []
|
|
173
|
+
})
|
|
174
|
+
state.sessionCache.set(sessionId, data)
|
|
175
|
+
await writeJson(sessionDataPath(sessionId), data)
|
|
176
|
+
}
|
|
177
|
+
state.index = next
|
|
178
|
+
await writeJson(indexFile, next)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function ensureLoadedUnsafe() {
|
|
182
|
+
if (state.loaded) return
|
|
183
|
+
await ensureUserRoot()
|
|
184
|
+
await ensureSessionShardRoot()
|
|
185
|
+
await migrateLegacyStoreIfNeededUnsafe()
|
|
186
|
+
state.loaded = true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureLoaded() {
|
|
190
|
+
return withLock(async () => {
|
|
191
|
+
await ensureLoadedUnsafe()
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function configureSessionStore(options = {}) {
|
|
196
|
+
if (typeof options.sessionShardEnabled === "boolean") {
|
|
197
|
+
state.options.sessionShardEnabled = options.sessionShardEnabled
|
|
198
|
+
}
|
|
199
|
+
if (Number.isInteger(options.flushIntervalMs) && options.flushIntervalMs >= 0) {
|
|
200
|
+
state.options.flushIntervalMs = options.flushIntervalMs
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function touchSession({
|
|
205
|
+
sessionId,
|
|
206
|
+
mode,
|
|
207
|
+
model,
|
|
208
|
+
providerType,
|
|
209
|
+
cwd,
|
|
210
|
+
title = null,
|
|
211
|
+
status = "active",
|
|
212
|
+
parentSessionId = null,
|
|
213
|
+
forkFrom = null
|
|
214
|
+
}) {
|
|
215
|
+
return withLock(async () => {
|
|
216
|
+
await ensureLoadedUnsafe()
|
|
217
|
+
const existing = state.index.sessions[sessionId]
|
|
218
|
+
const createdAt = existing?.createdAt || now()
|
|
219
|
+
state.index.sessions[sessionId] = {
|
|
220
|
+
id: sessionId,
|
|
221
|
+
mode,
|
|
222
|
+
model,
|
|
223
|
+
providerType,
|
|
224
|
+
cwd,
|
|
225
|
+
title: title || existing?.title || `${mode}:${model}`,
|
|
226
|
+
status,
|
|
227
|
+
parentSessionId: parentSessionId || existing?.parentSessionId || null,
|
|
228
|
+
forkFrom: forkFrom || existing?.forkFrom || null,
|
|
229
|
+
retryMeta: existing?.retryMeta || null,
|
|
230
|
+
patchRefs: existing?.patchRefs || [],
|
|
231
|
+
reviewDecisions: existing?.reviewDecisions || [],
|
|
232
|
+
budgetState: existing?.budgetState || null,
|
|
233
|
+
createdAt,
|
|
234
|
+
updatedAt: now()
|
|
235
|
+
}
|
|
236
|
+
await loadSessionDataUnsafe(sessionId)
|
|
237
|
+
markDirty(sessionId)
|
|
238
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
239
|
+
return state.index.sessions[sessionId]
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function updateSession(sessionId, patch) {
|
|
244
|
+
return withLock(async () => {
|
|
245
|
+
await ensureLoadedUnsafe()
|
|
246
|
+
const current = state.index.sessions[sessionId]
|
|
247
|
+
if (!current) return null
|
|
248
|
+
state.index.sessions[sessionId] = {
|
|
249
|
+
...current,
|
|
250
|
+
...patch,
|
|
251
|
+
updatedAt: now()
|
|
252
|
+
}
|
|
253
|
+
markDirty(sessionId)
|
|
254
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
255
|
+
return state.index.sessions[sessionId]
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function appendMessage(sessionId, role, content, extra = {}) {
|
|
260
|
+
return withLock(async () => {
|
|
261
|
+
await ensureLoadedUnsafe()
|
|
262
|
+
const data = await loadSessionDataUnsafe(sessionId)
|
|
263
|
+
const message = newMessage(role, content, extra)
|
|
264
|
+
data.messages.push(message)
|
|
265
|
+
if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
|
|
266
|
+
markDirty(sessionId)
|
|
267
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
268
|
+
return message
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function replaceMessages(sessionId, newMessages) {
|
|
273
|
+
return withLock(async () => {
|
|
274
|
+
await ensureLoadedUnsafe()
|
|
275
|
+
const data = await loadSessionDataUnsafe(sessionId)
|
|
276
|
+
data.messages = newMessages.map((m) => ({
|
|
277
|
+
...m,
|
|
278
|
+
id: m.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
279
|
+
timestamp: m.timestamp || now()
|
|
280
|
+
}))
|
|
281
|
+
if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
|
|
282
|
+
markDirty(sessionId)
|
|
283
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function appendPart(sessionId, part) {
|
|
288
|
+
return withLock(async () => {
|
|
289
|
+
await ensureLoadedUnsafe()
|
|
290
|
+
const data = await loadSessionDataUnsafe(sessionId)
|
|
291
|
+
const normalized = newPart(part.type || "event", part)
|
|
292
|
+
data.parts.push(normalized)
|
|
293
|
+
if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
|
|
294
|
+
markDirty(sessionId)
|
|
295
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
296
|
+
return normalized
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function getSession(sessionId) {
|
|
301
|
+
return withLock(async () => {
|
|
302
|
+
await ensureLoadedUnsafe()
|
|
303
|
+
await flushUnsafe()
|
|
304
|
+
const session = state.index.sessions[sessionId]
|
|
305
|
+
if (!session) return null
|
|
306
|
+
const data = await loadSessionDataUnsafe(sessionId)
|
|
307
|
+
return {
|
|
308
|
+
session,
|
|
309
|
+
messages: [...data.messages],
|
|
310
|
+
parts: [...data.parts]
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function listSessions({ cwd = null, limit = 100, includeChildren = true } = {}) {
|
|
316
|
+
return withLock(async () => {
|
|
317
|
+
await ensureLoadedUnsafe()
|
|
318
|
+
let sessions = Object.values(state.index.sessions)
|
|
319
|
+
if (cwd) sessions = sessions.filter((s) => s.cwd === cwd)
|
|
320
|
+
if (!includeChildren) sessions = sessions.filter((s) => !s.parentSessionId)
|
|
321
|
+
return sessions.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function getConversationHistory(sessionId, limit = 30) {
|
|
326
|
+
return withLock(async () => {
|
|
327
|
+
await ensureLoadedUnsafe()
|
|
328
|
+
const data = await loadSessionDataUnsafe(sessionId)
|
|
329
|
+
return data.messages.slice(-limit).map((msg) => ({
|
|
330
|
+
role: msg.role,
|
|
331
|
+
content: msg.content // preserves array content blocks (images) as-is
|
|
332
|
+
}))
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function markSessionStatus(sessionId, status) {
|
|
337
|
+
return updateSession(sessionId, { status })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function exportSession(sessionId) {
|
|
341
|
+
return getSession(sessionId)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function forkSession({ sessionId, newSessionId, title = null }) {
|
|
345
|
+
return withLock(async () => {
|
|
346
|
+
await ensureLoadedUnsafe()
|
|
347
|
+
const source = state.index.sessions[sessionId]
|
|
348
|
+
if (!source) return null
|
|
349
|
+
|
|
350
|
+
const sourceData = await loadSessionDataUnsafe(sessionId)
|
|
351
|
+
const child = {
|
|
352
|
+
...source,
|
|
353
|
+
id: newSessionId,
|
|
354
|
+
parentSessionId: source.id,
|
|
355
|
+
forkFrom: source.id,
|
|
356
|
+
title: title || `${source.title} (fork)`,
|
|
357
|
+
createdAt: now(),
|
|
358
|
+
updatedAt: now()
|
|
359
|
+
}
|
|
360
|
+
state.index.sessions[newSessionId] = child
|
|
361
|
+
state.sessionCache.set(newSessionId, {
|
|
362
|
+
messages: sourceData.messages.map((m) => ({ ...m })),
|
|
363
|
+
parts: sourceData.parts.map((p) => ({ ...p }))
|
|
364
|
+
})
|
|
365
|
+
markDirty(newSessionId)
|
|
366
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
367
|
+
return child
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function applyReviewDecision(sessionId, decision) {
|
|
372
|
+
return withLock(async () => {
|
|
373
|
+
await ensureLoadedUnsafe()
|
|
374
|
+
const session = state.index.sessions[sessionId]
|
|
375
|
+
if (!session) return null
|
|
376
|
+
session.reviewDecisions = session.reviewDecisions || []
|
|
377
|
+
session.reviewDecisions.push({
|
|
378
|
+
id: `rev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
379
|
+
createdAt: now(),
|
|
380
|
+
...decision
|
|
381
|
+
})
|
|
382
|
+
session.updatedAt = now()
|
|
383
|
+
markDirty(sessionId)
|
|
384
|
+
if (state.options.flushIntervalMs <= 0) await flushUnsafe()
|
|
385
|
+
return session
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function setBudgetState(sessionId, budgetState) {
|
|
390
|
+
return updateSession(sessionId, { budgetState })
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function appendUserMessage(sessionId, content, extra = {}) {
|
|
394
|
+
return appendMessage(sessionId, "user", content, extra)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function appendAssistantMessage(sessionId, content, extra = {}) {
|
|
398
|
+
return appendMessage(sessionId, "assistant", content, extra)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function fsckSessionStore() {
|
|
402
|
+
return withLock(async () => {
|
|
403
|
+
await ensureLoadedUnsafe()
|
|
404
|
+
await flushUnsafe()
|
|
405
|
+
|
|
406
|
+
const report = {
|
|
407
|
+
ok: true,
|
|
408
|
+
checkedAt: now(),
|
|
409
|
+
sessionsInIndex: Object.keys(state.index.sessions).length,
|
|
410
|
+
filesOnDisk: 0,
|
|
411
|
+
missingDataFiles: [],
|
|
412
|
+
orphanDataFiles: [],
|
|
413
|
+
invalidDataFiles: [],
|
|
414
|
+
suggestions: []
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const entries = await readdir(sessionShardRootPath(), { withFileTypes: true }).catch(() => [])
|
|
418
|
+
const diskSessionIds = entries
|
|
419
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json")
|
|
420
|
+
.map((entry) => path.basename(entry.name, ".json"))
|
|
421
|
+
report.filesOnDisk = diskSessionIds.length
|
|
422
|
+
|
|
423
|
+
const indexIds = new Set(Object.keys(state.index.sessions))
|
|
424
|
+
for (const sessionId of indexIds) {
|
|
425
|
+
const file = sessionDataPath(sessionId)
|
|
426
|
+
if (!(await exists(file))) {
|
|
427
|
+
report.missingDataFiles.push(sessionId)
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
const parsed = await readJson(file, null)
|
|
431
|
+
if (!parsed || !Array.isArray(parsed.messages) || !Array.isArray(parsed.parts)) {
|
|
432
|
+
report.invalidDataFiles.push(sessionId)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const sessionId of diskSessionIds) {
|
|
437
|
+
if (!indexIds.has(sessionId)) {
|
|
438
|
+
report.orphanDataFiles.push(sessionId)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (report.missingDataFiles.length || report.orphanDataFiles.length || report.invalidDataFiles.length) {
|
|
443
|
+
report.ok = false
|
|
444
|
+
if (report.missingDataFiles.length) report.suggestions.push("Run `kkcode session gc` to remove broken index entries.")
|
|
445
|
+
if (report.orphanDataFiles.length) report.suggestions.push("Run `kkcode session gc --orphans-only` to clean orphan session files.")
|
|
446
|
+
if (report.invalidDataFiles.length) report.suggestions.push("Backup invalid files then remove or restore them from snapshot.")
|
|
447
|
+
} else {
|
|
448
|
+
report.suggestions.push("No consistency issue detected.")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return report
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function gcSessionStore({ orphansOnly = false, maxAgeDays = 30 } = {}) {
|
|
456
|
+
return withLock(async () => {
|
|
457
|
+
await ensureLoadedUnsafe()
|
|
458
|
+
await flushUnsafe()
|
|
459
|
+
|
|
460
|
+
const removed = {
|
|
461
|
+
orphanFiles: [],
|
|
462
|
+
staleSessions: [],
|
|
463
|
+
checkpointDirs: []
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const entries = await readdir(sessionShardRootPath(), { withFileTypes: true }).catch(() => [])
|
|
467
|
+
const diskSessionIds = entries
|
|
468
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json")
|
|
469
|
+
.map((entry) => path.basename(entry.name, ".json"))
|
|
470
|
+
const indexIds = new Set(Object.keys(state.index.sessions))
|
|
471
|
+
|
|
472
|
+
for (const sessionId of diskSessionIds) {
|
|
473
|
+
if (!indexIds.has(sessionId)) {
|
|
474
|
+
await unlink(sessionDataPath(sessionId)).catch(() => {})
|
|
475
|
+
state.sessionCache.delete(sessionId)
|
|
476
|
+
removed.orphanFiles.push(sessionId)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!orphansOnly) {
|
|
481
|
+
const cutoff = now() - Math.max(1, Number(maxAgeDays || 30)) * 24 * 60 * 60 * 1000
|
|
482
|
+
const removableStatuses = new Set(["completed", "error", "stopped", "max-iterations", "no-progress", "heartbeat-timeout", "cancelled"])
|
|
483
|
+
for (const [sessionId, session] of Object.entries(state.index.sessions)) {
|
|
484
|
+
if (session.updatedAt > cutoff) continue
|
|
485
|
+
if (!removableStatuses.has(session.status)) continue
|
|
486
|
+
delete state.index.sessions[sessionId]
|
|
487
|
+
state.sessionCache.delete(sessionId)
|
|
488
|
+
await unlink(sessionDataPath(sessionId)).catch(() => {})
|
|
489
|
+
removed.staleSessions.push(sessionId)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const checkpointEntries = await readdir(sessionCheckpointRootPath(), { withFileTypes: true }).catch(() => [])
|
|
494
|
+
const liveSessionIds = new Set(Object.keys(state.index.sessions))
|
|
495
|
+
for (const entry of checkpointEntries) {
|
|
496
|
+
if (!entry.isDirectory()) continue
|
|
497
|
+
const sessionId = entry.name
|
|
498
|
+
if (liveSessionIds.has(sessionId)) continue
|
|
499
|
+
await rm(path.join(sessionCheckpointRootPath(), sessionId), { recursive: true, force: true }).catch(() => {})
|
|
500
|
+
removed.checkpointDirs.push(sessionId)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
state.dirtyIndex = true
|
|
504
|
+
await flushUnsafe()
|
|
505
|
+
return {
|
|
506
|
+
removed,
|
|
507
|
+
totalRemoved: removed.orphanFiles.length + removed.staleSessions.length + removed.checkpointDirs.length
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
}
|