@100xprompt/brain 0.0.0-stage → 0.1.0

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 (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,9 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { reader } from "./reader.ts"
3
+ import { kimiCodeNativeWriter } from "./writer.ts"
4
+
5
+ // Kimi Code (Moonshot, the TypeScript successor of the archived Kimi CLI): reads ~/.kimi-code sessions, and
6
+ // continues any session as a new native Kimi Code session that `kimi --session <id>` opens.
7
+ export { reader }
8
+ export const readers: Reader[] = [reader]
9
+ export const writers: Writer[] = [kimiCodeNativeWriter]
@@ -0,0 +1,92 @@
1
+ import { statSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { exists, gitInfo, isDir, ls, readJson, readJsonl } from "../../fsutil.ts"
4
+ import { header, iso, lossCounter } from "../../session/util.ts"
5
+ import type { Reader, SessionRef } from "../types.ts"
6
+ import { LABEL, str, TOOL, wireToTurns } from "./records.ts"
7
+
8
+ // Kimi Code keeps each session in ~/.kimi-code/sessions/<wd_slug_hash>/<sessionId>/: state.json and the main
9
+ // agent's journal, agents/main/wire.jsonl (records.ts). Subagents have journals of their own beside it.
10
+ // The archived Python Kimi CLI (~/.kimi) is not read: Kimi Code's own `kimi migrate` converts it.
11
+
12
+ /** Wire protocol versions this reader was checked against (Kimi Code migrates 1.0–1.4 up to 1.5). */
13
+ const KNOWN_WIRE = new Set(["1.0", "1.1", "1.2", "1.3", "1.4", "1.5"])
14
+
15
+ type State = Record<string, any>
16
+
17
+ function sessionDirs(home: string): string[] {
18
+ const root = join(home, ".kimi-code", "sessions")
19
+ const out: string[] = []
20
+ for (const ws of ls(root)) {
21
+ if (!ws.startsWith("wd_") || !isDir(join(root, ws))) continue
22
+ for (const id of ls(join(root, ws))) {
23
+ const dir = join(root, ws, id)
24
+ if (exists(join(dir, "state.json"))) out.push(dir)
25
+ }
26
+ }
27
+ return out
28
+ }
29
+
30
+ const mainWire = (dir: string, state: State | undefined) => {
31
+ const given = str(state?.agents?.main?.homedir)
32
+ // Kimi Code 0.38.0 records the agent folder relative to the session folder ("agents/main"); older ones an absolute path.
33
+ const home = given ? resolve(dir, given) : undefined
34
+ return home && exists(join(home, "wire.jsonl")) ? join(home, "wire.jsonl") : join(dir, "agents", "main", "wire.jsonl")
35
+ }
36
+
37
+ export const reader: Reader = {
38
+ tool: TOOL,
39
+ label: LABEL,
40
+
41
+ async list(home) {
42
+ const refs: SessionRef[] = []
43
+ for (const dir of sessionDirs(home)) {
44
+ const state = readJson<State>(join(dir, "state.json"))
45
+ const wire = mainWire(dir, state)
46
+ let size = 0
47
+ try {
48
+ size = statSync(wire).size
49
+ } catch {
50
+ continue
51
+ }
52
+ // A session never prompted has only its metadata line.
53
+ if (!str(state?.lastPrompt) && size < 256) continue
54
+ const sourceId = str(state?.id) ?? dir.split("/").pop()!
55
+ const title = str(state?.title)
56
+ const cwd = str(state?.cwd)
57
+ const updatedAt = iso(state?.updatedAt)
58
+ refs.push({ tool: TOOL, sourceId, path: wire, ...(title ? { title } : {}), ...(cwd ? { cwd } : {}), ...(updatedAt ? { updatedAt } : {}), extra: { dir } })
59
+ }
60
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
61
+ },
62
+
63
+ async read(ref, home) {
64
+ if (!exists(ref.path)) throw new Error(`${ref.path} does not exist`)
65
+ const dir = typeof ref.extra?.dir === "string" ? ref.extra.dir : join(ref.path, "..", "..", "..")
66
+ const state = readJson<State>(join(dir, "state.json"))
67
+ const { rows, bad } = readJsonl(ref.path)
68
+ if (!rows.length) throw new Error(`${ref.path} has no Kimi Code wire records`)
69
+ const lost = lossCounter()
70
+ if (bad) lost.add("unreadable lines", bad)
71
+ const meta = rows[0]?.type === "metadata" ? rows[0] : undefined
72
+ if (meta && !KNOWN_WIRE.has(String(meta.protocol_version))) lost.add(`unverified wire protocol ${String(meta.protocol_version)}`)
73
+
74
+ const cwd = str(state?.cwd) ?? ref.cwd
75
+ const title = str(state?.title)
76
+ const startedAt = iso(state?.createdAt) ?? iso(meta?.created_at)
77
+ const updatedAt = iso(state?.updatedAt)
78
+ const h = header(TOOL, str(state?.id) ?? ref.sourceId, {
79
+ sourcePath: ref.path,
80
+ ...(title ? { title } : {}),
81
+ project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd, home) },
82
+ ...(startedAt ? { startedAt } : {}),
83
+ ...(updatedAt ? { updatedAt } : {}),
84
+ })
85
+ const turns = wireToTurns(rows, h, lost)
86
+ // Subagents keep their own journals; their results stay in the parent's tool results.
87
+ const subs = Object.entries(state?.agents ?? {}).filter(([id, a]) => id !== "main" && (a as State)?.type === "sub").length
88
+ if (subs) lost.add("subagent transcripts (their results stay in the parent's tool results)", subs)
89
+ lost.flush(h)
90
+ return { header: h, turns }
91
+ },
92
+ }
@@ -0,0 +1,221 @@
1
+ import { createHash } from "node:crypto"
2
+ import { keepImage, parseDataUrl } from "../../session/attachments.ts"
3
+ import type { Attachment, SessionHeader, Turn } from "../../session/types.ts"
4
+ import { clip, iso, noteModel, toolKindOf, type lossCounter } from "../../session/util.ts"
5
+
6
+ // Facts about Kimi Code's session store, shared by its reader and writer. They come from Kimi Code's source
7
+ // (MoonshotAI/kimi-code, packages/agent-core-v2: wire/, agent/contextMemory/, agent/replayBuilder/fold.ts,
8
+ // _base/utils/workdir-slug.ts; packages/migration-legacy: the writer Kimi uses for sessions converted from
9
+ // elsewhere) and from the bundle of @moonshot-ai/kimi-code 2.5.2, which wrote and listed sessions here.
10
+ //
11
+ // ~/.kimi-code/sessions/<wd_slug_hash>/<sessionId>/ holds state.json (title, cwd, times, agents) and
12
+ // agents/<agentId>/wire.jsonl, the journal every resume replays: a metadata line, then turn.* records and
13
+ // context.* records. Messages arrive two ways: context.append_message (user prompts, and every message of
14
+ // a converted session), and context.append_loop_event (a live model step: step.begin, content.part,
15
+ // tool.call, step.end, tool.result). ~/.kimi-code/session_index.jsonl maps ids to folders.
16
+
17
+ export const TOOL = "kimi-code"
18
+ export const LABEL = "Kimi Code"
19
+
20
+ /** The wire protocol Kimi Code 2.5.2 writes (createWireMetadataRecord). */
21
+ export const WIRE_PROTOCOL = "1.5"
22
+ /** state.json's `version` in Kimi Code 2.5.2. */
23
+ export const META_VERSION = 2
24
+
25
+ type Lost = ReturnType<typeof lossCounter>
26
+ type Json = Record<string, any>
27
+ const isObj = (v: unknown): v is Json => typeof v === "object" && v !== null && !Array.isArray(v)
28
+
29
+ /** The bucket folder for a working directory, `wd_<slug>_<sha256[:12]>`, as encodeWorkDirKey makes it. */
30
+ export function workDirKey(workDir: string): string {
31
+ const normalized = workDir.replaceAll("\\", "/").replace(/\/+$/, "")
32
+ const base = normalized.split("/").pop() ?? normalized
33
+ let slug = base
34
+ .toLowerCase()
35
+ .replaceAll(/[^a-z0-9._-]+/g, "-")
36
+ .replaceAll(/^-+|-+$/g, "")
37
+ .slice(0, 40)
38
+ .replaceAll(/^-+|-+$/g, "")
39
+ if (slug === "" || slug === "." || slug === "..") slug = "workspace"
40
+ return `wd_${slug}_${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`
41
+ }
42
+
43
+ /**
44
+ * Origins whose user-role messages are not anyone's request: reminders and other injected context
45
+ * (injection), system wake-ups, hook output, task notices and retries. A compaction summary is its own turn.
46
+ */
47
+ const CONTEXT_ORIGINS = new Set(["injection", "system_trigger", "hook_result", "task", "retry", "cron_missed"])
48
+
49
+ /** Tool calls as wire 1.1+ stores them ({name, arguments}) or as 1.0 did ({function: {name, arguments}}). */
50
+ function callOf(c: Json): { id: string; name: string; input: unknown } | undefined {
51
+ if (typeof c.id !== "string" || !c.id) return undefined
52
+ const fn = isObj(c.function) ? c.function : c
53
+ const name = typeof fn.name === "string" && fn.name ? fn.name : "unknown"
54
+ return { id: c.id, name, input: parseArgs(fn.arguments ?? fn.args) }
55
+ }
56
+
57
+ function parseArgs(a: unknown): unknown {
58
+ if (typeof a !== "string") return a ?? {}
59
+ try {
60
+ return JSON.parse(a)
61
+ } catch {
62
+ return { arguments: a }
63
+ }
64
+ }
65
+
66
+ /** Text of a message's parts; the other kinds are counted as lost. */
67
+ function partsText(parts: unknown, lost: Lost, kinds: { think?: string[] } = {}): { text: string; attachments: Attachment[] } {
68
+ const texts: string[] = []
69
+ const attachments: Attachment[] = []
70
+ for (const p of Array.isArray(parts) ? parts : typeof parts === "string" ? [{ type: "text", text: parts }] : []) {
71
+ if (!isObj(p)) continue
72
+ if (p.type === "text" && typeof p.text === "string") texts.push(p.text)
73
+ else if (p.type === "think") {
74
+ if (typeof p.encrypted === "string" && p.encrypted) lost.add("signed or encrypted reasoning")
75
+ else if (typeof p.think === "string" && p.think.trim() && kinds.think) kinds.think.push(p.think)
76
+ } else if (p.type === "image_url") {
77
+ const url = isObj(p.imageUrl) ? p.imageUrl.url : undefined
78
+ const d = parseDataUrl(url)
79
+ const kept = d ? keepImage(d.base64, d.mime) : undefined
80
+ if (kept) attachments.push(kept)
81
+ else {
82
+ attachments.push({ kind: "image" })
83
+ lost.add("images")
84
+ }
85
+ } else if (p.type === "audio_url" || p.type === "video_url") lost.add("audio and video parts")
86
+ }
87
+ return { text: texts.join(""), attachments }
88
+ }
89
+
90
+ const toolOutput = (output: unknown, lost: Lost) => (typeof output === "string" ? output : partsText(output, lost).text)
91
+
92
+ /**
93
+ * The turns of one agent's wire records, in order: messages appended directly and the model steps folded
94
+ * from loop events, compactions as compaction turns, undone exchanges removed.
95
+ */
96
+ export function wireToTurns(records: Json[], h: SessionHeader, lost: Lost): Turn[] {
97
+ const turns: Turn[] = []
98
+ const calls = new Set<string>()
99
+ const answered = new Set<string>()
100
+ let step: Extract<Turn, { kind: "assistant" }> | undefined
101
+ const at = (r: Json) => iso(typeof r.time === "number" ? r.time : undefined)
102
+
103
+ const call = (id: string, name: string, input: unknown, when?: string) => {
104
+ if (calls.has(id)) return
105
+ calls.add(id)
106
+ turns.push({ kind: "tool_call", ...(when ? { at: when } : {}), id, name, toolKind: toolKindOf(name), input })
107
+ }
108
+ const result = (id: string, output: string, ok: boolean, when?: string) => {
109
+ if (answered.has(id) || !calls.has(id)) return
110
+ answered.add(id)
111
+ const c = clip(output)
112
+ turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: id, ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
113
+ }
114
+
115
+ for (const r of records) {
116
+ const when = at(r)
117
+ switch (r.type) {
118
+ // A live session records the provider model on each request (messages carry none).
119
+ case "llm.request":
120
+ if (typeof r.model === "string") noteModel(h, r.model)
121
+ break
122
+ case "context.append_message": {
123
+ const m = isObj(r.message) ? r.message : {}
124
+ if (m.role === "user") {
125
+ const origin = isObj(m.origin) ? m.origin.kind : undefined
126
+ if (origin === "compaction_summary") {
127
+ const s = partsText(m.content, lost).text
128
+ if (s.trim()) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary: s })
129
+ break
130
+ }
131
+ if (typeof origin === "string" && CONTEXT_ORIGINS.has(origin)) break
132
+ const { text, attachments } = partsText(m.content, lost)
133
+ // Sessions migrated from the Python Kimi CLI carry its reminders as plain user messages.
134
+ if (/^\s*<system-reminder>[\s\S]*<\/system-reminder>\s*$/.test(text)) break
135
+ if (text.trim() || attachments.length) turns.push({ kind: "user", ...(when ? { at: when } : {}), text, ...(attachments.length ? { attachments } : {}) })
136
+ } else if (m.role === "assistant") {
137
+ const think: string[] = []
138
+ const { text } = partsText(m.content, lost, { think })
139
+ for (const t of think) turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: t })
140
+ if (text.trim()) turns.push({ kind: "assistant", ...(when ? { at: when } : {}), text })
141
+ for (const c of Array.isArray(m.toolCalls) ? m.toolCalls : []) {
142
+ const v = isObj(c) ? callOf(c) : undefined
143
+ if (v) call(v.id, v.name, v.input, when)
144
+ }
145
+ } else if (m.role === "tool" && typeof m.toolCallId === "string") {
146
+ const out = toolOutput(m.content, lost)
147
+ // The Python Kimi CLI marked a failure in the text itself; migrated sessions keep that.
148
+ result(m.toolCallId, out, m.isError !== true && !out.startsWith("<system>ERROR"), when)
149
+ }
150
+ break
151
+ }
152
+ case "context.append_loop_event": {
153
+ const e = isObj(r.event) ? r.event : {}
154
+ if (e.type === "step.begin") step = undefined
155
+ else if (e.type === "content.part" && isObj(e.part)) {
156
+ const p = e.part
157
+ if (p.type === "text" && typeof p.text === "string") {
158
+ if (!step) {
159
+ step = { kind: "assistant", ...(when ? { at: when } : {}), text: "" }
160
+ turns.push(step)
161
+ }
162
+ step.text += p.text
163
+ } else {
164
+ const think: string[] = []
165
+ partsText([p], lost, { think })
166
+ for (const t of think) turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: t })
167
+ }
168
+ } else if (e.type === "tool.call" && typeof e.toolCallId === "string") {
169
+ call(e.toolCallId, typeof e.name === "string" ? e.name : "unknown", parseArgs(e.args), when)
170
+ } else if (e.type === "tool.result" && typeof e.toolCallId === "string" && isObj(e.result)) {
171
+ result(e.toolCallId, toolOutput(e.result.output, lost), e.result.isError !== true, when)
172
+ } else if (e.type === "step.end") {
173
+ if (step && isObj(e.usage) && step.text.trim()) {
174
+ const u = { input: e.usage.inputOther, output: e.usage.output, cacheRead: e.usage.inputCacheRead, cacheWrite: e.usage.inputCacheCreation }
175
+ const usage = Object.fromEntries(Object.entries(u).filter(([, v]) => typeof v === "number"))
176
+ if (Object.keys(usage).length) step.usage = usage
177
+ }
178
+ if (step && !step.text.trim()) turns.splice(turns.indexOf(step), 1)
179
+ step = undefined
180
+ }
181
+ break
182
+ }
183
+ case "context.apply_compaction": {
184
+ const s = typeof r.contextSummary === "string" ? r.contextSummary : typeof r.summary === "string" ? r.summary : isObj(r.summary) ? partsText(r.summary.content, lost).text : ""
185
+ if (s.trim()) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary: s })
186
+ break
187
+ }
188
+ case "context.undo": {
189
+ // Undo removes the last `count` exchanges, each back to its user message.
190
+ let left = typeof r.count === "number" ? r.count : 0
191
+ while (left > 0 && turns.length) {
192
+ const t = turns.pop()!
193
+ if (t.kind === "compaction") {
194
+ turns.push(t)
195
+ break
196
+ }
197
+ if (t.kind === "user") left--
198
+ }
199
+ break
200
+ }
201
+ case "context.clear":
202
+ lost.add("context clears (the turns before them are kept)")
203
+ break
204
+ }
205
+ }
206
+ // Calls the journal never answered get a result saying so, as Kimi does on resume, so every call is paired.
207
+ const open = [...calls].filter((id) => !answered.has(id) && turns.some((t) => t.kind === "tool_call" && t.id === id))
208
+ if (open.length) lost.add("tool results not recorded", open.length)
209
+ for (const id of open) {
210
+ const i = turns.findIndex((t) => t.kind === "tool_call" && t.id === id)
211
+ turns.splice(i + 1, 0, { kind: "tool_result", callId: id, ok: false, output: "(no result recorded)" })
212
+ }
213
+ if (!h.title) {
214
+ const first = turns.find((t) => t.kind === "user")
215
+ if (first?.kind === "user") h.title = first.text.replace(/\s+/g, " ").trim().slice(0, 120) || undefined
216
+ }
217
+ return turns
218
+ }
219
+
220
+ /** A state.json value as text, for titles and paths. */
221
+ export const str = (v: unknown) => (typeof v === "string" && v.trim() ? v : undefined)