pull/8586/head
Dax Raad 2026-01-16 15:13:00 -05:00
parent a4183c3b2c
commit 38f735bfc6
17 changed files with 277 additions and 118 deletions

View File

@ -326,7 +326,6 @@
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
"trust": "0.1.0",
"turndown": "7.2.0",
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
@ -3780,8 +3779,6 @@
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"trust": ["trust@0.1.0", "", {}, "sha512-BzU8tL0AD8ftb9008U9Wv6ww+ha5Z9fwQMmU0ICTTMKlazW3vGZkNCwX5L6t3Mf08vSA+aDCGsGjDT9nxl0vig=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],

View File

@ -14,7 +14,6 @@ CREATE TABLE `project` (
CREATE TABLE `message` (
`id` text PRIMARY KEY NOT NULL,
`session_id` text NOT NULL,
`created_at` integer NOT NULL,
`data` text NOT NULL,
FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade
);
@ -46,9 +45,24 @@ CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`parent_id` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
`data` text NOT NULL,
`slug` text NOT NULL,
`directory` text NOT NULL,
`title` text NOT NULL,
`version` text NOT NULL,
`share_url` text,
`summary_additions` integer,
`summary_deletions` integer,
`summary_files` integer,
`summary_diffs` text,
`revert_message_id` text,
`revert_part_id` text,
`revert_snapshot` text,
`revert_diff` text,
`permission` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`time_compacting` integer,
`time_archived` integer,
FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint

View File

@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "f79c82ae-4de1-4a4c-a5f3-857bc3ee97f2",
"id": "86d0107e-84d7-4b08-8411-afab7d6b1ee2",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"project": {
@ -101,13 +101,6 @@
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"data": {
"name": "data",
"type": "text",
@ -311,26 +304,131 @@
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"data": {
"name": "data",
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"directory": {
"name": "directory",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"version": {
"name": "version",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"share_url": {
"name": "share_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"summary_additions": {
"name": "summary_additions",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"summary_deletions": {
"name": "summary_deletions",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"summary_files": {
"name": "summary_files",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"summary_diffs": {
"name": "summary_diffs",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"revert_message_id": {
"name": "revert_message_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"revert_part_id": {
"name": "revert_part_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"revert_snapshot": {
"name": "revert_snapshot",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"revert_diff": {
"name": "revert_diff",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_updated": {
"name": "time_updated",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_compacting": {
"name": "time_compacting",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_archived": {
"name": "time_archived",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {

View File

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1768501411495,
"tag": "0000_easy_albert_cleary",
"when": 1768518709430,
"tag": "0000_vengeful_the_watchers",
"breakpoints": true
}
]

View File

View File

@ -31,7 +31,7 @@ export const migrations: { name: string; sql: string }[] = []
process.exit(0)
}
const imports = files.map((f, i) => `import m${i} from "../../drizzle/${f}" with { type: "text" }`).join("\n")
const imports = files.map((f, i) => `import m${i} from "../../migration/${f}" with { type: "text" }`).join("\n")
const entries = files.map((f, i) => ` { name: "${path.basename(f, ".sql")}", sql: m${i} },`).join("\n")

View File

View File

View File

@ -82,14 +82,31 @@ export const ImportCommand = cmd({
return
}
db()
.insert(SessionTable)
.values(Session.toRow({ ...exportData.info, projectID: Instance.project.id }))
.onConflictDoUpdate({
target: SessionTable.id,
set: Session.toRow({ ...exportData.info, projectID: Instance.project.id }),
})
.run()
const info = exportData.info
const row = {
id: info.id,
projectID: Instance.project.id,
parentID: info.parentID,
slug: info.slug,
directory: info.directory,
title: info.title,
version: info.version,
share_url: info.share?.url,
summary_additions: info.summary?.additions,
summary_deletions: info.summary?.deletions,
summary_files: info.summary?.files,
summary_diffs: info.summary?.diffs,
revert_messageID: info.revert?.messageID,
revert_partID: info.revert?.partID,
revert_snapshot: info.revert?.snapshot,
revert_diff: info.revert?.diff,
permission: info.permission,
time_created: info.time.created,
time_updated: info.time.updated,
time_compacting: info.time.compacting,
time_archived: info.time.archived,
}
db().insert(SessionTable).values(row).onConflictDoUpdate({ target: SessionTable.id, set: row }).run()
for (const msg of exportData.messages) {
db()
@ -97,7 +114,6 @@ export const ImportCommand = cmd({
.values({
id: msg.info.id,
sessionID: exportData.info.id,
createdAt: msg.info.time?.created ?? Date.now(),
data: msg.info,
})
.onConflictDoUpdate({ target: MessageTable.id, set: { data: msg.info } })

View File

@ -9,7 +9,7 @@ import { SessionTable } from "../session/session.sql"
import { eq } from "drizzle-orm"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { Session } from "../session"
import { work } from "../util/queue"
import { fn } from "@opencode-ai/util/fn"
import { BusEvent } from "@/bus/bus-event"
@ -304,13 +304,10 @@ export namespace Project {
log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
await work(10, globalSessions, async (row) => {
const session = Session.fromRow(row)
if (!session) return
if (session.directory && session.directory !== worktree) return
if (row.directory && row.directory !== worktree) return
session.projectID = newProjectID
log.info("migrating session", { sessionID: session.id, from: "global", to: newProjectID })
db().update(SessionTable).set(Session.toRow(session)).where(eq(SessionTable.id, session.id)).run()
log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID })
db().update(SessionTable).set({ projectID: newProjectID }).where(eq(SessionTable.id, row.id)).run()
}).catch((error) => {
log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
})

View File

@ -85,32 +85,6 @@ export namespace Session {
}
}
export function toRow(info: Info) {
return {
id: info.id,
projectID: info.projectID,
parentID: info.parentID,
slug: info.slug,
directory: info.directory,
title: info.title,
version: info.version,
share_url: info.share?.url,
summary_additions: info.summary?.additions,
summary_deletions: info.summary?.deletions,
summary_files: info.summary?.files,
summary_diffs: info.summary?.diffs,
revert_messageID: info.revert?.messageID,
revert_partID: info.revert?.partID,
revert_snapshot: info.revert?.snapshot,
revert_diff: info.revert?.diff,
permission: info.permission,
time_created: info.time.created,
time_updated: info.time.updated,
time_compacting: info.time.compacting,
time_archived: info.time.archived,
}
}
export const Info = z
.object({
id: Identifier.schema("session"),
@ -256,9 +230,10 @@ export namespace Session {
)
export const touch = fn(Identifier.schema("session"), async (sessionID) => {
await update(sessionID, (draft) => {
draft.time.updated = Date.now()
})
const now = Date.now()
db().update(SessionTable).set({ time_updated: now }).where(eq(SessionTable.id, sessionID)).run()
const session = await get(sessionID)
Bus.publish(Event.Updated, { info: session })
})
export async function createNext(input: {
@ -283,21 +258,29 @@ export namespace Session {
},
}
log.info("created", result)
db().insert(SessionTable).values(toRow(result)).run()
db()
.insert(SessionTable)
.values({
id: result.id,
projectID: result.projectID,
parentID: result.parentID,
slug: result.slug,
directory: result.directory,
title: result.title,
version: result.version,
permission: result.permission,
time_created: result.time.created,
time_updated: result.time.updated,
})
.run()
Bus.publish(Event.Created, {
info: result,
})
const cfg = await Config.get()
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
share(result.id)
.then((share) => {
update(result.id, (draft) => {
draft.share = share
})
})
.catch(() => {
// Silently ignore sharing errors during session creation
})
share(result.id).catch(() => {
// Silently ignore sharing errors during session creation
})
Bus.publish(Event.Updated, {
info: result,
})
@ -404,7 +387,6 @@ export namespace Session {
})
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
const project = Instance.project
try {
const session = await get(sessionID)
for (const child of await children(sessionID)) {
@ -422,13 +404,11 @@ export namespace Session {
})
export const updateMessage = fn(MessageV2.Info, async (msg) => {
const createdAt = msg.role === "user" ? msg.time.created : msg.time.created
db()
.insert(MessageTable)
.values({
id: msg.id,
sessionID: msg.sessionID,
createdAt,
data: msg,
})
.onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } })

View File

@ -166,10 +166,13 @@ export namespace SessionPrompt {
})
}
if (permissions.length > 0) {
session.permission = permissions
await Session.update(session.id, (draft) => {
draft.permission = permissions
})
Session.update(
session.id,
(draft) => {
draft.permission = permissions
},
{ touch: false },
)
}
if (input.noReply === true) {

View File

@ -6,7 +6,7 @@ import { Session } from "."
import { Log } from "../util/log"
import { splitWhen } from "remeda"
import { db } from "../storage/db"
import { MessageTable, PartTable } from "./session.sql"
import { MessageTable, PartTable, SessionTable, SessionDiffTable } from "./session.sql"
import { eq } from "drizzle-orm"
import { Bus } from "../bus"
import { SessionPrompt } from "./prompt"
@ -56,13 +56,17 @@ export namespace SessionRevert {
}
if (revert) {
const session = await Session.get(input.sessionID)
revert.snapshot = session.revert?.snapshot ?? (await Snapshot.track())
const current = await Session.get(input.sessionID)
revert.snapshot = current.revert?.snapshot ?? (await Snapshot.track())
await Snapshot.revert(patches)
if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot)
const rangeMessages = all.filter((msg) => msg.info.id >= revert!.messageID)
const diffs = await SessionSummary.computeDiff({ messages: rangeMessages })
await Storage.write(["session_diff", input.sessionID], diffs)
db()
.insert(SessionDiffTable)
.values({ sessionID: input.sessionID, data: diffs })
.onConflictDoUpdate({ target: SessionDiffTable.sessionID, set: { data: diffs } })
.run()
Bus.publish(Session.Event.Diff, {
sessionID: input.sessionID,
diff: diffs,
@ -85,10 +89,21 @@ export namespace SessionRevert {
const session = await Session.get(input.sessionID)
if (!session.revert) return session
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)
const next = await Session.update(input.sessionID, (draft) => {
draft.revert = undefined
})
return next
const now = Date.now()
db()
.update(SessionTable)
.set({
revert_messageID: null,
revert_partID: null,
revert_snapshot: null,
revert_diff: null,
time_updated: now,
})
.where(eq(SessionTable.id, input.sessionID))
.run()
const updated = await Session.get(input.sessionID)
Bus.publish(Session.Event.Updated, { info: updated })
return updated
}
export async function cleanup(session: Session.Info) {
@ -116,8 +131,19 @@ export namespace SessionRevert {
})
}
}
await Session.update(sessionID, (draft) => {
draft.revert = undefined
})
const now = Date.now()
db()
.update(SessionTable)
.set({
revert_messageID: null,
revert_partID: null,
revert_snapshot: null,
revert_diff: null,
time_updated: now,
})
.where(eq(SessionTable.id, sessionID))
.run()
const updated = await Session.get(sessionID)
Bus.publish(Session.Event.Updated, { info: updated })
}
}

View File

@ -12,7 +12,7 @@ import { Log } from "@/util/log"
import path from "path"
import { Instance } from "@/project/instance"
import { db } from "@/storage/db"
import { SessionDiffTable } from "./session.sql"
import { SessionDiffTable, SessionTable } from "./session.sql"
import { eq } from "drizzle-orm"
import { Bus } from "@/bus"
@ -49,13 +49,19 @@ export namespace SessionSummary {
return files.has(x.file)
}),
)
await Session.update(input.sessionID, (draft) => {
draft.summary = {
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
files: diffs.length,
}
})
const now = Date.now()
db()
.update(SessionTable)
.set({
summary_additions: diffs.reduce((sum, x) => sum + x.additions, 0),
summary_deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
summary_files: diffs.length,
time_updated: now,
})
.where(eq(SessionTable.id, input.sessionID))
.run()
const session = await Session.get(input.sessionID)
Bus.publish(Session.Event.Updated, { info: session })
db()
.insert(SessionDiffTable)
.values({ sessionID: input.sessionID, data: diffs })

View File

@ -145,7 +145,6 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin
.values({
id: data.id,
sessionID: data.sessionID,
createdAt: data.time?.created ?? Date.now(),
data,
})
.onConflictDoNothing()

View File

@ -1,4 +1,6 @@
// Auto-generated - do not edit
import m0 from "../../migration/0000_easy_albert_cleary.sql" with { type: "text" }
import m0 from "../../migration/0000_vengeful_the_watchers.sql" with { type: "text" }
export const migrations = [{ name: "0000_easy_albert_cleary", sql: m0 }]
export const migrations = [
{ name: "0000_vengeful_the_watchers", sql: m0 },
]

View File

@ -0,0 +1,21 @@
import { AsyncLocalStorage } from "node:async_hooks"
export namespace Context {
export class NotFound extends Error {}
export function create<T>() {
const storage = new AsyncLocalStorage<T>()
return {
use() {
const result = storage.getStore()
if (!result) {
throw new NotFound()
}
return result
},
provide<R>(value: T, fn: () => R) {
return storage.run(value, fn)
},
}
}
}