@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.
- package/LICENSE +48 -0
- package/README.md +172 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto"
|
|
3
|
+
import { chmodSync, linkSync, lstatSync, mkdirSync, rmSync } from "node:fs"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
import { toolLabel } from "../../brief.ts"
|
|
6
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
7
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
8
|
+
import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
|
|
9
|
+
import type { Session, ToolKind, Turn, Usage } from "../../session/types.ts"
|
|
10
|
+
import { countTurns, unpaired } from "../../session/util.ts"
|
|
11
|
+
import { continuedNote } from "../opencode/note.ts"
|
|
12
|
+
import { asRecord, safeToolName } from "../opencode/tools.ts"
|
|
13
|
+
import { callTitle, continuedTitle, sh } from "../opencode/writer.ts"
|
|
14
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
15
|
+
import { readDevin } from "./reader.ts"
|
|
16
|
+
import {
|
|
17
|
+
COLUMNS,
|
|
18
|
+
COMPACTION_EXT,
|
|
19
|
+
DEFAULT_MODEL,
|
|
20
|
+
LABEL,
|
|
21
|
+
MIGRATIONS,
|
|
22
|
+
SCHEMA,
|
|
23
|
+
SUMMARY_PREFIX,
|
|
24
|
+
TESTED_VERSIONS,
|
|
25
|
+
TOOL,
|
|
26
|
+
acpKindOf,
|
|
27
|
+
devinBin,
|
|
28
|
+
devinVersion,
|
|
29
|
+
imageSize,
|
|
30
|
+
schemaProblem,
|
|
31
|
+
storePath,
|
|
32
|
+
type DevinImage,
|
|
33
|
+
type DevinMessage,
|
|
34
|
+
type DevinMessageMeta,
|
|
35
|
+
type DevinToolCall,
|
|
36
|
+
} from "./records.ts"
|
|
37
|
+
|
|
38
|
+
// Continues a portable session in Devin CLI as a new session of its shared sessions.db, written as the rows
|
|
39
|
+
// Devin 3000.6.7 writes itself (the native-corpus capture): a sessions row and one message_nodes chain from the
|
|
40
|
+
// root to sessions.main_chain_id. The chain opens with brain's note as a system message; then a user node per
|
|
41
|
+
// prompt (images as base64 `images`), an assistant node per model response (its text, its readable reasoning as
|
|
42
|
+
// `thinking`, its `tool_calls`), and a tool node per result, with the ACP tool_call_state Devin keeps for each
|
|
43
|
+
// call. Prompts go to prompt_history, as session-migrate's writer does. A compaction becomes a system node
|
|
44
|
+
// marked "brain/compaction" (Devin's own summary form is not known), and reads back as a compaction.
|
|
45
|
+
//
|
|
46
|
+
// The write is one BEGIN IMMEDIATE transaction that refuses an id the store already has and reads the session
|
|
47
|
+
// back before committing; the store is backed up first (VACUUM INTO, a consistent copy even while Devin runs).
|
|
48
|
+
// The doc does not say Devin holds the store open, and SQLite's locking covers a concurrent writer, so a running
|
|
49
|
+
// Devin is not refused. A missing store is built with the exact schema-16 text and migrations Devin makes, under
|
|
50
|
+
// a temporary name, and linked into place only if no store appeared meanwhile.
|
|
51
|
+
|
|
52
|
+
/** The message_nodes.metadata Devin writes for a node that is not part of its system prefix. */
|
|
53
|
+
const NODE_META = JSON.stringify({ summarized_from: null, num_tokens_preceding: null, is_system_prefix: null })
|
|
54
|
+
|
|
55
|
+
export interface NodeRow {
|
|
56
|
+
node_id: number
|
|
57
|
+
parent_node_id: number | null
|
|
58
|
+
chat_message: DevinMessage
|
|
59
|
+
created_at: number
|
|
60
|
+
metadata: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DevinPlan {
|
|
64
|
+
id: string
|
|
65
|
+
/** The sessions row, by column. */
|
|
66
|
+
session: Record<string, string | number | null>
|
|
67
|
+
nodes: NodeRow[]
|
|
68
|
+
prompts: Array<{ content: string; timestamp: number }>
|
|
69
|
+
states: Array<{ tool_call_id: string; tool_call_json: string; tool_call_update_json: string }>
|
|
70
|
+
lost: string[]
|
|
71
|
+
/** What a read-back must find. */
|
|
72
|
+
expect: { users: number; assistants: number; calls: number; reasoning: number; compactions: number }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A new session id in Devin's slug style that says where the session came from, e.g. "from-claude-code-3f9a1c". */
|
|
76
|
+
export function newSessionId(session: Session, taken: Set<string>): string {
|
|
77
|
+
const from = session.header.tool.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+/, "").slice(0, 40) || "session"
|
|
78
|
+
for (;;) {
|
|
79
|
+
const id = `from-${from}-${randomBytes(3).toString("hex")}`
|
|
80
|
+
if (!taken.has(id)) return id
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function metaOf(
|
|
85
|
+
source: string,
|
|
86
|
+
operation: string,
|
|
87
|
+
at: string,
|
|
88
|
+
o: { userInput?: boolean; finish?: string; tokens?: number; extensions?: Record<string, unknown>; model?: string } = {},
|
|
89
|
+
): DevinMessageMeta {
|
|
90
|
+
return {
|
|
91
|
+
num_tokens: o.tokens ?? null,
|
|
92
|
+
is_user_input: o.userInput ?? null,
|
|
93
|
+
request_id: null,
|
|
94
|
+
metrics: null,
|
|
95
|
+
finish_reason: o.finish ?? null,
|
|
96
|
+
...(o.extensions ? { extensions: o.extensions } : {}),
|
|
97
|
+
...(o.model !== undefined ? { started_generation_at: at } : {}),
|
|
98
|
+
created_at: at,
|
|
99
|
+
...(o.model !== undefined ? { generation_model: o.model } : {}),
|
|
100
|
+
telemetry: { source, operation },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const FILE_KEYS = ["file_path", "filePath", "path", "target_file", "absolute_path", "file"]
|
|
105
|
+
|
|
106
|
+
/** A portable call as Devin draws it: an ACP title, kind and the file it touches. */
|
|
107
|
+
function acpView(name: string, kind: ToolKind, input: Record<string, unknown>) {
|
|
108
|
+
const file = ["read", "edit", "write"].includes(kind) ? FILE_KEYS.map((k) => input[k]).find((v): v is string => typeof v === "string" && !!v) : undefined
|
|
109
|
+
const title = kind === "read" ? "Read file" : kind === "edit" || kind === "write" ? "Edit file" : callTitle(name, input)
|
|
110
|
+
return { title, kind: acpKindOf(kind), ...(file ? { locations: [{ path: file }] } : {}) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The rows of a new Devin session continuing `session`. Pure: writes nothing. */
|
|
114
|
+
export function planDevin(session: Session, opts: { id: string; cwd: string; now: Date }): DevinPlan {
|
|
115
|
+
const drops = new Map<string, number>()
|
|
116
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
117
|
+
const nowSec = Math.floor(opts.now.getTime() / 1000)
|
|
118
|
+
const nodes: NodeRow[] = []
|
|
119
|
+
const prompts: DevinPlan["prompts"] = []
|
|
120
|
+
const states: DevinPlan["states"] = []
|
|
121
|
+
const expect = { users: 0, assistants: 0, calls: 0, reasoning: 0, compactions: 0 }
|
|
122
|
+
|
|
123
|
+
// Node times are whole seconds that never go back and never pass now; the JSON keeps the turn's own time when it fits.
|
|
124
|
+
let clock = Math.min(nowSec, Math.floor((Date.parse(session.header.startedAt ?? "") || opts.now.getTime()) / 1000))
|
|
125
|
+
const tick = (at?: string): string => {
|
|
126
|
+
const ms = at ? Date.parse(at) : NaN
|
|
127
|
+
const t = Number.isFinite(ms) ? Math.floor(ms / 1000) : NaN
|
|
128
|
+
if (t > clock) clock = Math.min(t, nowSec)
|
|
129
|
+
return t === clock ? new Date(ms).toISOString() : new Date(clock * 1000).toISOString()
|
|
130
|
+
}
|
|
131
|
+
const push = (chat_message: DevinMessage): NodeRow => {
|
|
132
|
+
const node: NodeRow = { node_id: nodes.length + 1, parent_node_id: nodes.length || null, chat_message, created_at: clock, metadata: NODE_META }
|
|
133
|
+
nodes.push(node)
|
|
134
|
+
return node
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sourceModel = session.header.models.at(-1)
|
|
138
|
+
const devinModel = sourceModel && /^swe-/i.test(sourceModel) ? sourceModel : DEFAULT_MODEL
|
|
139
|
+
/** The model of the response being written: the first reply's to begin with. */
|
|
140
|
+
let model = session.turns.find((t): t is Extract<Turn, { kind: "assistant" }> => t.kind === "assistant" && !!t.model)?.model ?? sourceModel ?? devinModel
|
|
141
|
+
|
|
142
|
+
interface Call {
|
|
143
|
+
id: string
|
|
144
|
+
name: string
|
|
145
|
+
kind: string
|
|
146
|
+
view: ReturnType<typeof acpView>
|
|
147
|
+
input: Record<string, unknown>
|
|
148
|
+
}
|
|
149
|
+
const calls = new Map<string, Call>()
|
|
150
|
+
const answered = new Set<string>()
|
|
151
|
+
const meta = (c: Call) => ({ "cognition.ai/inferenceToolName": c.name })
|
|
152
|
+
|
|
153
|
+
const result = (src: string, ok: boolean, output: string, at?: string) => {
|
|
154
|
+
const c = calls.get(src)!
|
|
155
|
+
answered.add(src)
|
|
156
|
+
const when = tick(at)
|
|
157
|
+
push({
|
|
158
|
+
message_id: randomUUID(),
|
|
159
|
+
role: "tool",
|
|
160
|
+
content: output,
|
|
161
|
+
tool_call_id: c.id,
|
|
162
|
+
metadata: metaOf("tool_result", c.name, when, {
|
|
163
|
+
extensions: {
|
|
164
|
+
"chisel/tool_result_meta": ok ? { success: true, kind: c.kind } : { success: false, failure_reason: "ToolError", kind: c.kind },
|
|
165
|
+
...(ok ? {} : { "chisel/tool_failure": { reason: "ToolError" } }),
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
})
|
|
169
|
+
states.push({
|
|
170
|
+
tool_call_id: c.id,
|
|
171
|
+
tool_call_json: JSON.stringify({ toolCallId: c.id, title: c.view.title, kind: c.view.kind, ...(c.view.locations ? { locations: c.view.locations } : {}), rawInput: c.input, _meta: meta(c) }),
|
|
172
|
+
tool_call_update_json: JSON.stringify({
|
|
173
|
+
toolCallId: c.id,
|
|
174
|
+
status: ok ? "completed" : "failed",
|
|
175
|
+
...(ok ? {} : { content: [{ type: "content", content: { type: "text", text: output } }] }),
|
|
176
|
+
_meta: meta(c),
|
|
177
|
+
}),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface Step {
|
|
182
|
+
node: NodeRow
|
|
183
|
+
at: string
|
|
184
|
+
model: string
|
|
185
|
+
text: string[]
|
|
186
|
+
thinking: string[]
|
|
187
|
+
calls: DevinToolCall[]
|
|
188
|
+
sources: string[]
|
|
189
|
+
shown: Record<string, unknown>
|
|
190
|
+
usage?: Usage
|
|
191
|
+
results: boolean
|
|
192
|
+
}
|
|
193
|
+
let step: Step | undefined
|
|
194
|
+
const pending: Array<() => void> = []
|
|
195
|
+
|
|
196
|
+
const openStep = (at?: string): Step => {
|
|
197
|
+
if (step && !step.results) return step
|
|
198
|
+
closeStep()
|
|
199
|
+
const when = tick(at)
|
|
200
|
+
// Placeholders in Devin's key order; closeStep fills them.
|
|
201
|
+
const node = push({ message_id: randomUUID(), role: "assistant", content: "", tool_calls: [], thinking: null, metadata: metaOf("assistant", "inference", when) })
|
|
202
|
+
step = { node, at: when, model, text: [], thinking: [], calls: [], sources: [], shown: {}, results: false }
|
|
203
|
+
return step
|
|
204
|
+
}
|
|
205
|
+
function closeStep() {
|
|
206
|
+
if (!step) return
|
|
207
|
+
const s = step
|
|
208
|
+
step = undefined
|
|
209
|
+
const m = s.node.chat_message
|
|
210
|
+
m.content = s.text.join("\n\n")
|
|
211
|
+
m.tool_calls = s.calls
|
|
212
|
+
m.thinking = s.thinking.length ? { thinking: s.thinking.join("\n\n"), signature: "" } : null
|
|
213
|
+
m.metadata = metaOf("assistant", "inference", s.at, {
|
|
214
|
+
finish: s.calls.length ? "tool_calls" : "stop",
|
|
215
|
+
...(s.usage?.output !== undefined ? { tokens: s.usage.output } : {}),
|
|
216
|
+
model: s.model,
|
|
217
|
+
...(s.calls.length ? { extensions: { "chisel/tool_call_content": s.shown } } : {}),
|
|
218
|
+
})
|
|
219
|
+
if (s.text.length) expect.assistants++
|
|
220
|
+
if (s.thinking.length) expect.reasoning++
|
|
221
|
+
for (const r of pending.splice(0)) r()
|
|
222
|
+
// Every call needs its result before the next message, as the model's API requires.
|
|
223
|
+
for (const src of s.sources)
|
|
224
|
+
if (!answered.has(src)) {
|
|
225
|
+
result(src, false, "No result was recorded for this call.")
|
|
226
|
+
drop("calls with no result, marked as failed")
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
push({
|
|
231
|
+
message_id: randomUUID(),
|
|
232
|
+
role: "system",
|
|
233
|
+
content: continuedNote(toolLabel(session.header.tool), session.header.id, opts.now.toISOString().slice(0, 10)),
|
|
234
|
+
metadata: metaOf("system", "unknown", tick(session.header.startedAt)),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
for (const t of session.turns as Turn[]) {
|
|
238
|
+
switch (t.kind) {
|
|
239
|
+
case "user": {
|
|
240
|
+
const images: DevinImage[] = []
|
|
241
|
+
let skipped = 0
|
|
242
|
+
for (const a of t.attachments ?? []) {
|
|
243
|
+
const url = a.kind === "image" ? parseDataUrl(imageDataUrl(a)) : undefined
|
|
244
|
+
if (!url) {
|
|
245
|
+
skipped++
|
|
246
|
+
drop("attachments (not images, or their files are gone)")
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
images.push({ ...imageSize(Buffer.from(url.base64, "base64")), base64_data: url.base64, mime_type: url.mime, source_path: a.path ?? a.name ?? "", caption: "" })
|
|
250
|
+
}
|
|
251
|
+
const text = t.text.trim() ? t.text : skipped ? `(${skipped} attachment${skipped > 1 ? "s" : ""} not carried over)` : ""
|
|
252
|
+
if (!text && !images.length) break
|
|
253
|
+
closeStep()
|
|
254
|
+
const when = tick(t.at)
|
|
255
|
+
push({
|
|
256
|
+
message_id: randomUUID(),
|
|
257
|
+
role: "user",
|
|
258
|
+
content: text,
|
|
259
|
+
...(images.length ? { images } : {}),
|
|
260
|
+
metadata: metaOf("user", "unknown", when, { userInput: true, ...(text ? { extensions: { "chisel/acp-content-blocks": [{ type: "text", text }] } } : {}) }),
|
|
261
|
+
})
|
|
262
|
+
if (text.trim()) prompts.push({ content: text, timestamp: clock })
|
|
263
|
+
expect.users++
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
case "assistant": {
|
|
267
|
+
if (!t.text.trim()) break
|
|
268
|
+
if (t.model && t.model !== model) {
|
|
269
|
+
// A response belongs to one model: text from another starts a new one (its reasoning stays with it).
|
|
270
|
+
if (step && (step.text.length || step.calls.length)) closeStep()
|
|
271
|
+
model = t.model
|
|
272
|
+
if (step) step.model = model
|
|
273
|
+
}
|
|
274
|
+
const s = openStep(t.at)
|
|
275
|
+
s.text.push(t.text)
|
|
276
|
+
if (t.usage) s.usage = t.usage
|
|
277
|
+
// Devin's metrics block holds timings this session does not have; only the output count is kept (num_tokens).
|
|
278
|
+
if (t.usage && (t.usage.input !== undefined || t.usage.cacheRead !== undefined || t.usage.cacheWrite !== undefined)) drop("token usage of replies, other than the output count")
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
case "reasoning":
|
|
282
|
+
if (!t.text.trim()) break
|
|
283
|
+
openStep(t.at).thinking.push(t.text)
|
|
284
|
+
drop("reasoning: kept as text, without provider signatures")
|
|
285
|
+
break
|
|
286
|
+
case "tool_call": {
|
|
287
|
+
if (calls.has(t.id)) {
|
|
288
|
+
drop("repeated tool call ids")
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
const s = openStep(t.at)
|
|
292
|
+
const raw = t.input
|
|
293
|
+
if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) drop("tool inputs that were not objects (wrapped)")
|
|
294
|
+
const input = asRecord(raw)
|
|
295
|
+
const name = safeToolName(t.name)
|
|
296
|
+
if (name !== t.name) drop("tool names changed to fit Devin")
|
|
297
|
+
const view = acpView(name, t.toolKind, input)
|
|
298
|
+
const id = `call_${randomBytes(12).toString("hex")}`
|
|
299
|
+
calls.set(t.id, { id, name, kind: view.kind, view, input })
|
|
300
|
+
s.calls.push({ id, name, arguments: input, index: s.calls.length, kind: "function" })
|
|
301
|
+
s.sources.push(t.id)
|
|
302
|
+
s.shown[id] = { toolCallId: id, title: view.title, status: "pending", ...(view.locations ? { locations: view.locations } : {}), kind: view.kind, rawInput: input }
|
|
303
|
+
expect.calls++
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
case "tool_result": {
|
|
307
|
+
if (!calls.has(t.callId)) {
|
|
308
|
+
drop("tool results without a call")
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
// A result belongs right after the response that made its call; one that comes later (after the next
|
|
312
|
+
// message, when its call was already closed as failed) has nowhere to go.
|
|
313
|
+
if (answered.has(t.callId) || !step?.sources.includes(t.callId)) {
|
|
314
|
+
drop("repeated tool results, or results that came after the conversation moved on")
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
const src = t.callId
|
|
318
|
+
answered.add(src)
|
|
319
|
+
step.results = true
|
|
320
|
+
pending.push(() => result(src, t.ok, t.output, t.at))
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case "compaction": {
|
|
324
|
+
if (!t.summary.trim()) break
|
|
325
|
+
closeStep()
|
|
326
|
+
const when = tick(t.at)
|
|
327
|
+
push({ message_id: randomUUID(), role: "system", content: `${SUMMARY_PREFIX}${t.summary}`, metadata: metaOf("system", "unknown", when, { extensions: { [COMPACTION_EXT]: true } }) })
|
|
328
|
+
expect.compactions++
|
|
329
|
+
drop("compactions: kept as a marked system message, the turns before them stay in the history")
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
case "file_edit":
|
|
333
|
+
if (!t.callId) drop("file edits not made by a tool call")
|
|
334
|
+
break
|
|
335
|
+
case "subagent":
|
|
336
|
+
drop("subagent sessions (their call and result are kept)")
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
closeStep()
|
|
341
|
+
|
|
342
|
+
const lost = [...drops].map(([what, n]) => `${what} (${n})`)
|
|
343
|
+
if (calls.size) lost.push(`tool-call ids replaced (${calls.size})`)
|
|
344
|
+
if (sourceModel && devinModel !== sourceModel) lost.push(`the model: Devin continues on ${devinModel} (the session used ${sourceModel})`)
|
|
345
|
+
const first = nodes[0]!
|
|
346
|
+
const last = nodes.at(-1)!
|
|
347
|
+
return {
|
|
348
|
+
id: opts.id,
|
|
349
|
+
session: {
|
|
350
|
+
id: opts.id,
|
|
351
|
+
working_directory: opts.cwd,
|
|
352
|
+
backend_type: "windsurf",
|
|
353
|
+
model: devinModel,
|
|
354
|
+
agent_mode: "accept-edits",
|
|
355
|
+
created_at: first.created_at,
|
|
356
|
+
last_activity_at: last.created_at,
|
|
357
|
+
title: continuedTitle(session),
|
|
358
|
+
main_chain_id: last.node_id,
|
|
359
|
+
shell_last_seen_index: 0,
|
|
360
|
+
cogs_json: "[]",
|
|
361
|
+
workspace_dirs: "[]",
|
|
362
|
+
hidden: 0,
|
|
363
|
+
metadata: JSON.stringify({ total_credit_cost: 0, total_acu_cost: 0, response_dimensions: [] }),
|
|
364
|
+
},
|
|
365
|
+
nodes,
|
|
366
|
+
prompts,
|
|
367
|
+
states,
|
|
368
|
+
lost,
|
|
369
|
+
expect,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Inserts the plan in one BEGIN IMMEDIATE transaction and reads it back before committing; any failure rolls it back. */
|
|
374
|
+
export function insertPlan(db: Database, plan: DevinPlan, path: string) {
|
|
375
|
+
const cols = COLUMNS.sessions!
|
|
376
|
+
// Prepared statements are finalized here: one left to the garbage collector keeps the connection open.
|
|
377
|
+
const stmts = [
|
|
378
|
+
db.prepare(`INSERT INTO sessions (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`),
|
|
379
|
+
db.prepare("INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?)"),
|
|
380
|
+
db.prepare("INSERT INTO prompt_history (content, timestamp, session_id, is_shell) VALUES (?, ?, ?, 0)"),
|
|
381
|
+
db.prepare("INSERT INTO tool_call_state (session_id, tool_call_id, tool_call_json, tool_call_update_json) VALUES (?, ?, ?, ?)"),
|
|
382
|
+
] as const
|
|
383
|
+
const [session, node, prompt, state] = stmts
|
|
384
|
+
db.exec("BEGIN IMMEDIATE")
|
|
385
|
+
try {
|
|
386
|
+
if (db.query("SELECT 1 FROM sessions WHERE id = ?").get(plan.id)) throw new Error(`refusing to overwrite Devin session ${plan.id}: the store already has it`)
|
|
387
|
+
session.run(...cols.map((c) => plan.session[c] ?? null))
|
|
388
|
+
for (const n of plan.nodes) node.run(plan.id, n.node_id, n.parent_node_id, JSON.stringify(n.chat_message), n.created_at, n.metadata)
|
|
389
|
+
for (const p of plan.prompts) prompt.run(p.content, p.timestamp, plan.id)
|
|
390
|
+
for (const s of plan.states) state.run(plan.id, s.tool_call_id, s.tool_call_json, s.tool_call_update_json)
|
|
391
|
+
// Read back as Devin's sessions are read.
|
|
392
|
+
const back = readDevin(db, plan.id, path)
|
|
393
|
+
const n = countTurns(back)
|
|
394
|
+
const pairs = unpaired(back)
|
|
395
|
+
const e = plan.expect
|
|
396
|
+
if (n.user !== e.users || n.assistant !== e.assistants || n.tool_call !== e.calls || n.reasoning !== e.reasoning || n.compaction !== e.compactions || pairs.calls.length || pairs.results.length)
|
|
397
|
+
throw new Error(
|
|
398
|
+
`the Devin session did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, reasoning ${n.reasoning}/${e.reasoning}, compactions ${n.compaction}/${e.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
|
|
399
|
+
)
|
|
400
|
+
db.exec("COMMIT")
|
|
401
|
+
} catch (err) {
|
|
402
|
+
db.exec("ROLLBACK")
|
|
403
|
+
throw err
|
|
404
|
+
} finally {
|
|
405
|
+
for (const st of stmts) st.finalize()
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Adds the session to an existing store after a consistent backup. */
|
|
410
|
+
export function addToStore(dbPath: string, plan: DevinPlan, backupFile: string) {
|
|
411
|
+
if (lstatSync(dbPath).isSymbolicLink()) throw new Error(`the Devin store ${dbPath} is a symbolic link; brain does not write through one`)
|
|
412
|
+
// readwrite without create: a store removed meanwhile is an error, not a new empty store.
|
|
413
|
+
const db = new Database(dbPath, { readwrite: true })
|
|
414
|
+
try {
|
|
415
|
+
db.exec("PRAGMA busy_timeout = 5000")
|
|
416
|
+
db.exec("PRAGMA foreign_keys = ON")
|
|
417
|
+
const problem = schemaProblem(db)
|
|
418
|
+
if (problem) throw new Error(problem)
|
|
419
|
+
mkdirSync(dirname(backupFile), { recursive: true, mode: 0o700 })
|
|
420
|
+
db.run("VACUUM INTO ?", [backupFile])
|
|
421
|
+
chmodSync(backupFile, 0o600)
|
|
422
|
+
insertPlan(db, plan, dbPath)
|
|
423
|
+
try {
|
|
424
|
+
db.exec("PRAGMA wal_checkpoint(PASSIVE)")
|
|
425
|
+
} catch {
|
|
426
|
+
// Devin checkpoints the log itself.
|
|
427
|
+
}
|
|
428
|
+
} finally {
|
|
429
|
+
db.close(true)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Builds a missing store as Devin 3000.6.7 does, with the session in it, and puts it in place atomically. */
|
|
434
|
+
export function createStore(dbPath: string, plan: DevinPlan, now: Date) {
|
|
435
|
+
const dir = dirname(dbPath)
|
|
436
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
437
|
+
const tmp = join(dir, `.sessions.${randomBytes(6).toString("hex")}.db.tmp`)
|
|
438
|
+
try {
|
|
439
|
+
const db = new Database(tmp)
|
|
440
|
+
try {
|
|
441
|
+
db.exec("PRAGMA journal_mode = WAL")
|
|
442
|
+
for (const sql of SCHEMA) db.exec(sql)
|
|
443
|
+
// refinery's applied_on: RFC 3339 with nanoseconds, a moment apart per migration.
|
|
444
|
+
const base = now.toISOString().slice(0, 23)
|
|
445
|
+
const add = db.prepare("INSERT INTO refinery_schema_history (version, name, applied_on, checksum) VALUES (?, ?, ?, ?)")
|
|
446
|
+
MIGRATIONS.forEach(([version, name, checksum], i) => add.run(version, name, `${base}${String(i * 1000).padStart(6, "0")}Z`, checksum))
|
|
447
|
+
add.finalize()
|
|
448
|
+
insertPlan(db, plan, dbPath)
|
|
449
|
+
// Everything into the main file: the -wal of the temporary name cannot follow the link.
|
|
450
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
451
|
+
} finally {
|
|
452
|
+
// Released now, before the file is linked into place: SQLite tracks a WAL store by its inode, and a
|
|
453
|
+
// connection left open on the temporary name would tie the store's shared memory to that name.
|
|
454
|
+
db.close(true)
|
|
455
|
+
}
|
|
456
|
+
chmodSync(tmp, 0o600)
|
|
457
|
+
try {
|
|
458
|
+
linkSync(tmp, dbPath)
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`a Devin store appeared at ${dbPath} while brain was writing; it was left as it is`)
|
|
461
|
+
throw err
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
for (const side of ["", "-wal", "-shm", "-journal"]) rmSync(tmp + side, { force: true })
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
|
|
469
|
+
|
|
470
|
+
export const devinWriter: Writer = {
|
|
471
|
+
tool: TOOL,
|
|
472
|
+
label: LABEL,
|
|
473
|
+
way: "native",
|
|
474
|
+
|
|
475
|
+
async ready(opts: WriteOptions): Promise<Readiness> {
|
|
476
|
+
const bin = devinBin()
|
|
477
|
+
if (!bin) return { ok: false, why: "Devin CLI is not installed (no devin on PATH)" }
|
|
478
|
+
const { raw, version } = devinVersion(bin)
|
|
479
|
+
if (!version || !TESTED_VERSIONS.includes(version))
|
|
480
|
+
return { ok: false, why: `Devin CLI ${version ?? raw ?? "(version unknown)"} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
481
|
+
if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
|
|
482
|
+
const db = storePath(opts.home)
|
|
483
|
+
if (exists(db)) {
|
|
484
|
+
let problem: string | undefined
|
|
485
|
+
try {
|
|
486
|
+
problem = withSnapshot(db, schemaProblem)
|
|
487
|
+
} catch (e) {
|
|
488
|
+
problem = `the Devin store at ${db} cannot be read: ${(e as Error).message}`
|
|
489
|
+
}
|
|
490
|
+
if (problem) return { ok: false, why: problem }
|
|
491
|
+
}
|
|
492
|
+
return { ok: true }
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
async write(session: Session, opts: WriteOptions): Promise<WriteResult> {
|
|
496
|
+
const cwd = resolve(opts.cwd)
|
|
497
|
+
const dbPath = storePath(opts.home)
|
|
498
|
+
const now = new Date()
|
|
499
|
+
const had = exists(dbPath)
|
|
500
|
+
let taken = new Set<string>()
|
|
501
|
+
if (had) {
|
|
502
|
+
const seen = withSnapshot(dbPath, (d) => ({ problem: schemaProblem(d), ids: (d.query("SELECT id FROM sessions").all() as Array<{ id: string }>).map((r) => r.id) }))
|
|
503
|
+
if (seen.problem) throw new Error(seen.problem)
|
|
504
|
+
taken = new Set(seen.ids)
|
|
505
|
+
}
|
|
506
|
+
const plan = planDevin(session, { id: newSessionId(session, taken), cwd, now })
|
|
507
|
+
const backup = join(opts.home, ".brain", "backups", TOOL, `${stamp(now)}-${plan.id}`, "sessions.db")
|
|
508
|
+
if (!opts.dryRun) {
|
|
509
|
+
if (had) addToStore(dbPath, plan, backup)
|
|
510
|
+
else createStore(dbPath, plan, now)
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
tool: TOOL,
|
|
514
|
+
way: "native",
|
|
515
|
+
sessionId: plan.id,
|
|
516
|
+
written: had ? [dbPath, backup] : [dbPath],
|
|
517
|
+
// `devin list` shows the sessions of the folder it runs in, so the session is resumed from its own.
|
|
518
|
+
open: `cd ${sh(cwd)} && devin --resume ${plan.id}`,
|
|
519
|
+
lost: plan.lost,
|
|
520
|
+
dryRun: !!opts.dryRun,
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { factoryDroidReader } from "./reader.ts"
|
|
3
|
+
import { factoryDroidWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Factory Droid (CLI): reads its sessions and continues any session as a new native Droid session,
|
|
6
|
+
// `droid --resume <id>`.
|
|
7
|
+
|
|
8
|
+
export { DROID_TESTED_VERSIONS, factoryDroidWriter } from "./writer.ts"
|
|
9
|
+
export { factoryDroidReader }
|
|
10
|
+
|
|
11
|
+
export const readers: Reader[] = [factoryDroidReader]
|
|
12
|
+
export const writers: Writer[] = [factoryDroidWriter()]
|