@kkelly-offical/kkcode 0.1.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.
Files changed (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,46 @@
1
+ You are kkcode in generic mode.
2
+
3
+ Priorities:
4
+ - correctness
5
+ - safety
6
+ - reproducibility
7
+
8
+ Always include:
9
+ - what was changed
10
+ - what remains
11
+ - how to verify
12
+
13
+ Codebase exploration discipline:
14
+ - Before modifying code, use `glob` and `grep` to understand the project structure and find related files.
15
+ - ALWAYS read a file before editing it. Never edit blind.
16
+ - When modifying a function, trace its callers with `grep` to ensure you update all call sites.
17
+ - When adding an import, verify the target module exists and exports the symbol.
18
+ - When working across multiple files, understand the dependency graph first.
19
+
20
+ Error handling discipline:
21
+ - When a tool call fails, read the error carefully. Do NOT retry the same action blindly.
22
+ - If an edit fails because the target string wasn't found, re-read the file — it may have changed.
23
+ - If a bash command fails, analyze stderr before retrying with a different approach.
24
+ - After fixing an error, verify the fix succeeded.
25
+
26
+ Testing and verification discipline:
27
+ - After modifying or writing code, ALWAYS run tests and syntax checks BEFORE declaring the task complete.
28
+ - For JavaScript/TypeScript projects:
29
+ - Run `node --check <file>` to verify JavaScript syntax
30
+ - Run `npx tsc --noEmit` if tsconfig.json exists to verify TypeScript types
31
+ - Run `npm test` to execute the test suite and verify changes don't break anything
32
+ - For Python projects:
33
+ - Run `python -m py_compile <file>` to verify Python syntax
34
+ - Run `python -m pytest` or similar test commands to verify changes
35
+
36
+ Tool usage rules:
37
+ - To create a new file, call `write` directly with the full content. NEVER use `task` for file creation. `write` auto-creates parent directories.
38
+ - To modify an existing file, call `edit` with before/after snippets. NEVER use `task` for simple edits.
39
+ - To read a file, call `read`. NEVER use `bash` with `cat`, `type`, `Get-Content`, `head`, `tail`, or similar commands to read files.
40
+ - To find files, call `glob`. To search content, call `grep`. NEVER use `bash` with `find` or `grep` commands.
41
+ - To run a shell command, call `bash` directly. NEVER wrap bash in `task`.
42
+ - Only use `task` for complex multi-step work that requires autonomous reasoning across many files.
43
+ - When creating multiple files, call `write` for each file sequentially — do NOT delegate to background tasks.
44
+ - Prefer `edit` over `write` when only a small part of a file needs to change.
45
+ - When writing large files, include ALL content in a single `write` call. Do NOT split into multiple writes or append later.
46
+ - NEVER run long-running or persistent commands via `bash`: `npm run dev`, `npm start`, `yarn dev`, `npx vite`, `webpack serve`, `nodemon`, `jest --watch`, `tsc --watch`, etc. These block execution indefinitely. Instead, tell the user to run them manually in their terminal. For tests, use single-run mode (e.g. `vitest --run`, `jest` without --watch).
@@ -0,0 +1,18 @@
1
+ import path from "node:path"
2
+ import { readFile } from "node:fs/promises"
3
+ import { fileURLToPath } from "node:url"
4
+ import { renderTemplate } from "../util/template.mjs"
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const PROMPT_DIR = path.join(__dirname, "prompt")
8
+
9
+ const cache = new Map()
10
+
11
+ export async function loadSessionPrompt(name, vars = {}) {
12
+ if (!cache.has(name)) {
13
+ const file = path.join(PROMPT_DIR, name)
14
+ const text = await readFile(file, "utf8")
15
+ cache.set(name, text.trim())
16
+ }
17
+ return renderTemplate(cache.get(name), vars)
18
+ }
@@ -0,0 +1,52 @@
1
+ import { updateSession, listSessions, getSession } from "./store.mjs"
2
+
3
+ export function isRecoveryEnabled(config = {}) {
4
+ return config?.session?.recovery !== false
5
+ }
6
+
7
+ export async function markTurnInProgress(sessionId, turnId, step = 0, enabled = true) {
8
+ if (!enabled) return
9
+ await updateSession(sessionId, {
10
+ retryMeta: {
11
+ inProgress: true,
12
+ turnId,
13
+ step,
14
+ updatedAt: Date.now()
15
+ }
16
+ })
17
+ }
18
+
19
+ export async function markTurnFinished(sessionId, enabled = true) {
20
+ if (!enabled) return
21
+ await updateSession(sessionId, {
22
+ retryMeta: {
23
+ inProgress: false,
24
+ updatedAt: Date.now()
25
+ }
26
+ })
27
+ }
28
+
29
+ export async function listRecoverableSessions({ cwd = null, limit = 50, enabled = true } = {}) {
30
+ if (!enabled) return []
31
+ const sessions = await listSessions({ cwd, limit })
32
+ return sessions.filter(
33
+ (session) => session.retryMeta?.inProgress || session.status === "error"
34
+ )
35
+ }
36
+
37
+ export async function getResumeContext(sessionId, { enabled = true } = {}) {
38
+ if (!enabled) return null
39
+ const data = await getSession(sessionId)
40
+ if (!data) return null
41
+ const { session, messages } = data
42
+ const userMessages = messages.filter((m) => m.role === "user" && !m.synthetic)
43
+ const lastUserMessage = userMessages.length ? userMessages[userMessages.length - 1] : null
44
+ return {
45
+ session,
46
+ lastPrompt: lastUserMessage?.content || null,
47
+ messageCount: messages.length,
48
+ retryMeta: session.retryMeta || null,
49
+ canResume: Boolean(lastUserMessage),
50
+ canRetry: Boolean(session.retryMeta?.failedAt)
51
+ }
52
+ }
@@ -0,0 +1,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
+ let lock = Promise.resolve()
84
+ function withLock(fn) {
85
+ const run = lock.then(fn, fn)
86
+ lock = run.then(
87
+ () => undefined,
88
+ () => undefined
89
+ )
90
+ return run
91
+ }
92
+
93
+ function scheduleFlush() {
94
+ if (state.options.flushIntervalMs <= 0) return
95
+ if (state.flushTimer) return
96
+ state.flushTimer = setTimeout(() => {
97
+ state.flushTimer = null
98
+ flushNow().catch((err) => {
99
+ console.error("[store] flush failed:", err?.message || err)
100
+ })
101
+ }, state.options.flushIntervalMs)
102
+ }
103
+
104
+ function markDirty(sessionId = null) {
105
+ state.dirtyIndex = true
106
+ if (sessionId) state.dirtySessions.add(sessionId)
107
+ if (state.options.flushIntervalMs <= 0) return
108
+ scheduleFlush()
109
+ }
110
+
111
+ async function flushUnsafe() {
112
+ if (!state.loaded) return
113
+ await ensureUserRoot()
114
+ await ensureSessionShardRoot()
115
+
116
+ for (const sessionId of [...state.dirtySessions]) {
117
+ const data = state.sessionCache.get(sessionId) || defaultSessionData()
118
+ await writeJson(sessionDataPath(sessionId), data)
119
+ state.dirtySessions.delete(sessionId)
120
+ }
121
+
122
+ if (state.dirtyIndex) {
123
+ state.index.updatedAt = now()
124
+ await writeJson(sessionIndexPath(), state.index)
125
+ state.dirtyIndex = false
126
+ }
127
+ }
128
+
129
+ export async function flushNow() {
130
+ return withLock(async () => {
131
+ await flushUnsafe()
132
+ })
133
+ }
134
+
135
+ async function loadSessionDataUnsafe(sessionId) {
136
+ if (state.sessionCache.has(sessionId)) {
137
+ return state.sessionCache.get(sessionId)
138
+ }
139
+ const data = normalizeSessionData(await readJson(sessionDataPath(sessionId), defaultSessionData()))
140
+ state.sessionCache.set(sessionId, data)
141
+ return data
142
+ }
143
+
144
+ async function migrateLegacyStoreIfNeededUnsafe() {
145
+ const indexFile = sessionIndexPath()
146
+ if (await exists(indexFile)) {
147
+ state.index = await readJson(indexFile, defaultIndex())
148
+ return
149
+ }
150
+
151
+ const legacy = await readJson(legacySessionStorePath(), null)
152
+ if (!legacy || typeof legacy !== "object" || !legacy.sessions || typeof legacy.sessions !== "object") {
153
+ state.index = defaultIndex()
154
+ await writeJson(indexFile, state.index)
155
+ return
156
+ }
157
+
158
+ const next = defaultIndex()
159
+ for (const [sessionId, session] of Object.entries(legacy.sessions || {})) {
160
+ next.sessions[sessionId] = {
161
+ ...session
162
+ }
163
+ const data = normalizeSessionData({
164
+ messages: legacy.messages?.[sessionId] || [],
165
+ parts: legacy.parts?.[sessionId] || []
166
+ })
167
+ state.sessionCache.set(sessionId, data)
168
+ await writeJson(sessionDataPath(sessionId), data)
169
+ }
170
+ state.index = next
171
+ await writeJson(indexFile, next)
172
+ }
173
+
174
+ async function ensureLoadedUnsafe() {
175
+ if (state.loaded) return
176
+ await ensureUserRoot()
177
+ await ensureSessionShardRoot()
178
+ await migrateLegacyStoreIfNeededUnsafe()
179
+ state.loaded = true
180
+ }
181
+
182
+ async function ensureLoaded() {
183
+ return withLock(async () => {
184
+ await ensureLoadedUnsafe()
185
+ })
186
+ }
187
+
188
+ export function configureSessionStore(options = {}) {
189
+ if (typeof options.sessionShardEnabled === "boolean") {
190
+ state.options.sessionShardEnabled = options.sessionShardEnabled
191
+ }
192
+ if (Number.isInteger(options.flushIntervalMs) && options.flushIntervalMs >= 0) {
193
+ state.options.flushIntervalMs = options.flushIntervalMs
194
+ }
195
+ }
196
+
197
+ export async function touchSession({
198
+ sessionId,
199
+ mode,
200
+ model,
201
+ providerType,
202
+ cwd,
203
+ title = null,
204
+ status = "active",
205
+ parentSessionId = null,
206
+ forkFrom = null
207
+ }) {
208
+ return withLock(async () => {
209
+ await ensureLoadedUnsafe()
210
+ const existing = state.index.sessions[sessionId]
211
+ const createdAt = existing?.createdAt || now()
212
+ state.index.sessions[sessionId] = {
213
+ id: sessionId,
214
+ mode,
215
+ model,
216
+ providerType,
217
+ cwd,
218
+ title: title || existing?.title || `${mode}:${model}`,
219
+ status,
220
+ parentSessionId: parentSessionId || existing?.parentSessionId || null,
221
+ forkFrom: forkFrom || existing?.forkFrom || null,
222
+ retryMeta: existing?.retryMeta || null,
223
+ patchRefs: existing?.patchRefs || [],
224
+ reviewDecisions: existing?.reviewDecisions || [],
225
+ budgetState: existing?.budgetState || null,
226
+ createdAt,
227
+ updatedAt: now()
228
+ }
229
+ await loadSessionDataUnsafe(sessionId)
230
+ markDirty(sessionId)
231
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
232
+ return state.index.sessions[sessionId]
233
+ })
234
+ }
235
+
236
+ export async function updateSession(sessionId, patch) {
237
+ return withLock(async () => {
238
+ await ensureLoadedUnsafe()
239
+ const current = state.index.sessions[sessionId]
240
+ if (!current) return null
241
+ state.index.sessions[sessionId] = {
242
+ ...current,
243
+ ...patch,
244
+ updatedAt: now()
245
+ }
246
+ markDirty(sessionId)
247
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
248
+ return state.index.sessions[sessionId]
249
+ })
250
+ }
251
+
252
+ export async function appendMessage(sessionId, role, content, extra = {}) {
253
+ return withLock(async () => {
254
+ await ensureLoadedUnsafe()
255
+ const data = await loadSessionDataUnsafe(sessionId)
256
+ const message = newMessage(role, content, extra)
257
+ data.messages.push(message)
258
+ if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
259
+ markDirty(sessionId)
260
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
261
+ return message
262
+ })
263
+ }
264
+
265
+ export async function replaceMessages(sessionId, newMessages) {
266
+ return withLock(async () => {
267
+ await ensureLoadedUnsafe()
268
+ const data = await loadSessionDataUnsafe(sessionId)
269
+ data.messages = newMessages.map((m) => ({
270
+ ...m,
271
+ id: m.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
272
+ timestamp: m.timestamp || now()
273
+ }))
274
+ if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
275
+ markDirty(sessionId)
276
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
277
+ })
278
+ }
279
+
280
+ export async function appendPart(sessionId, part) {
281
+ return withLock(async () => {
282
+ await ensureLoadedUnsafe()
283
+ const data = await loadSessionDataUnsafe(sessionId)
284
+ const normalized = newPart(part.type || "event", part)
285
+ data.parts.push(normalized)
286
+ if (state.index.sessions[sessionId]) state.index.sessions[sessionId].updatedAt = now()
287
+ markDirty(sessionId)
288
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
289
+ return normalized
290
+ })
291
+ }
292
+
293
+ export async function getSession(sessionId) {
294
+ return withLock(async () => {
295
+ await ensureLoadedUnsafe()
296
+ await flushUnsafe()
297
+ const session = state.index.sessions[sessionId]
298
+ if (!session) return null
299
+ const data = await loadSessionDataUnsafe(sessionId)
300
+ return {
301
+ session,
302
+ messages: [...data.messages],
303
+ parts: [...data.parts]
304
+ }
305
+ })
306
+ }
307
+
308
+ export async function listSessions({ cwd = null, limit = 100, includeChildren = true } = {}) {
309
+ return withLock(async () => {
310
+ await ensureLoadedUnsafe()
311
+ let sessions = Object.values(state.index.sessions)
312
+ if (cwd) sessions = sessions.filter((s) => s.cwd === cwd)
313
+ if (!includeChildren) sessions = sessions.filter((s) => !s.parentSessionId)
314
+ return sessions.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit)
315
+ })
316
+ }
317
+
318
+ export async function getConversationHistory(sessionId, limit = 30) {
319
+ return withLock(async () => {
320
+ await ensureLoadedUnsafe()
321
+ const data = await loadSessionDataUnsafe(sessionId)
322
+ return data.messages.slice(-limit).map((msg) => ({
323
+ role: msg.role,
324
+ content: msg.content // preserves array content blocks (images) as-is
325
+ }))
326
+ })
327
+ }
328
+
329
+ export async function markSessionStatus(sessionId, status) {
330
+ return updateSession(sessionId, { status })
331
+ }
332
+
333
+ export async function exportSession(sessionId) {
334
+ return getSession(sessionId)
335
+ }
336
+
337
+ export async function forkSession({ sessionId, newSessionId, title = null }) {
338
+ return withLock(async () => {
339
+ await ensureLoadedUnsafe()
340
+ const source = state.index.sessions[sessionId]
341
+ if (!source) return null
342
+
343
+ const sourceData = await loadSessionDataUnsafe(sessionId)
344
+ const child = {
345
+ ...source,
346
+ id: newSessionId,
347
+ parentSessionId: source.id,
348
+ forkFrom: source.id,
349
+ title: title || `${source.title} (fork)`,
350
+ createdAt: now(),
351
+ updatedAt: now()
352
+ }
353
+ state.index.sessions[newSessionId] = child
354
+ state.sessionCache.set(newSessionId, {
355
+ messages: sourceData.messages.map((m) => ({ ...m })),
356
+ parts: sourceData.parts.map((p) => ({ ...p }))
357
+ })
358
+ markDirty(newSessionId)
359
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
360
+ return child
361
+ })
362
+ }
363
+
364
+ export async function applyReviewDecision(sessionId, decision) {
365
+ return withLock(async () => {
366
+ await ensureLoadedUnsafe()
367
+ const session = state.index.sessions[sessionId]
368
+ if (!session) return null
369
+ session.reviewDecisions = session.reviewDecisions || []
370
+ session.reviewDecisions.push({
371
+ id: `rev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
372
+ createdAt: now(),
373
+ ...decision
374
+ })
375
+ session.updatedAt = now()
376
+ markDirty(sessionId)
377
+ if (state.options.flushIntervalMs <= 0) await flushUnsafe()
378
+ return session
379
+ })
380
+ }
381
+
382
+ export async function setBudgetState(sessionId, budgetState) {
383
+ return updateSession(sessionId, { budgetState })
384
+ }
385
+
386
+ export async function appendUserMessage(sessionId, content, extra = {}) {
387
+ return appendMessage(sessionId, "user", content, extra)
388
+ }
389
+
390
+ export async function appendAssistantMessage(sessionId, content, extra = {}) {
391
+ return appendMessage(sessionId, "assistant", content, extra)
392
+ }
393
+
394
+ export async function fsckSessionStore() {
395
+ return withLock(async () => {
396
+ await ensureLoadedUnsafe()
397
+ await flushUnsafe()
398
+
399
+ const report = {
400
+ ok: true,
401
+ checkedAt: now(),
402
+ sessionsInIndex: Object.keys(state.index.sessions).length,
403
+ filesOnDisk: 0,
404
+ missingDataFiles: [],
405
+ orphanDataFiles: [],
406
+ invalidDataFiles: [],
407
+ suggestions: []
408
+ }
409
+
410
+ const entries = await readdir(sessionShardRootPath(), { withFileTypes: true }).catch(() => [])
411
+ const diskSessionIds = entries
412
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json")
413
+ .map((entry) => path.basename(entry.name, ".json"))
414
+ report.filesOnDisk = diskSessionIds.length
415
+
416
+ const indexIds = new Set(Object.keys(state.index.sessions))
417
+ for (const sessionId of indexIds) {
418
+ const file = sessionDataPath(sessionId)
419
+ if (!(await exists(file))) {
420
+ report.missingDataFiles.push(sessionId)
421
+ continue
422
+ }
423
+ const parsed = await readJson(file, null)
424
+ if (!parsed || !Array.isArray(parsed.messages) || !Array.isArray(parsed.parts)) {
425
+ report.invalidDataFiles.push(sessionId)
426
+ }
427
+ }
428
+
429
+ for (const sessionId of diskSessionIds) {
430
+ if (!indexIds.has(sessionId)) {
431
+ report.orphanDataFiles.push(sessionId)
432
+ }
433
+ }
434
+
435
+ if (report.missingDataFiles.length || report.orphanDataFiles.length || report.invalidDataFiles.length) {
436
+ report.ok = false
437
+ if (report.missingDataFiles.length) report.suggestions.push("Run `kkcode session gc` to remove broken index entries.")
438
+ if (report.orphanDataFiles.length) report.suggestions.push("Run `kkcode session gc --orphans-only` to clean orphan session files.")
439
+ if (report.invalidDataFiles.length) report.suggestions.push("Backup invalid files then remove or restore them from snapshot.")
440
+ } else {
441
+ report.suggestions.push("No consistency issue detected.")
442
+ }
443
+
444
+ return report
445
+ })
446
+ }
447
+
448
+ export async function gcSessionStore({ orphansOnly = false, maxAgeDays = 30 } = {}) {
449
+ return withLock(async () => {
450
+ await ensureLoadedUnsafe()
451
+ await flushUnsafe()
452
+
453
+ const removed = {
454
+ orphanFiles: [],
455
+ staleSessions: [],
456
+ checkpointDirs: []
457
+ }
458
+
459
+ const entries = await readdir(sessionShardRootPath(), { withFileTypes: true }).catch(() => [])
460
+ const diskSessionIds = entries
461
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json")
462
+ .map((entry) => path.basename(entry.name, ".json"))
463
+ const indexIds = new Set(Object.keys(state.index.sessions))
464
+
465
+ for (const sessionId of diskSessionIds) {
466
+ if (!indexIds.has(sessionId)) {
467
+ await unlink(sessionDataPath(sessionId)).catch(() => {})
468
+ state.sessionCache.delete(sessionId)
469
+ removed.orphanFiles.push(sessionId)
470
+ }
471
+ }
472
+
473
+ if (!orphansOnly) {
474
+ const cutoff = now() - Math.max(1, Number(maxAgeDays || 30)) * 24 * 60 * 60 * 1000
475
+ const removableStatuses = new Set(["completed", "error", "stopped", "max-iterations", "no-progress", "heartbeat-timeout", "cancelled"])
476
+ for (const [sessionId, session] of Object.entries(state.index.sessions)) {
477
+ if (session.updatedAt > cutoff) continue
478
+ if (!removableStatuses.has(session.status)) continue
479
+ delete state.index.sessions[sessionId]
480
+ state.sessionCache.delete(sessionId)
481
+ await unlink(sessionDataPath(sessionId)).catch(() => {})
482
+ removed.staleSessions.push(sessionId)
483
+ }
484
+ }
485
+
486
+ const checkpointEntries = await readdir(sessionCheckpointRootPath(), { withFileTypes: true }).catch(() => [])
487
+ const liveSessionIds = new Set(Object.keys(state.index.sessions))
488
+ for (const entry of checkpointEntries) {
489
+ if (!entry.isDirectory()) continue
490
+ const sessionId = entry.name
491
+ if (liveSessionIds.has(sessionId)) continue
492
+ await rm(path.join(sessionCheckpointRootPath(), sessionId), { recursive: true, force: true }).catch(() => {})
493
+ removed.checkpointDirs.push(sessionId)
494
+ }
495
+
496
+ state.dirtyIndex = true
497
+ await flushUnsafe()
498
+ return {
499
+ removed,
500
+ totalRemoved: removed.orphanFiles.length + removed.staleSessions.length + removed.checkpointDirs.length
501
+ }
502
+ })
503
+ }