refactor(core): simplify persistence boundaries (#43929)

This commit is contained in:
Kit Langton
2026-08-21 13:48:27 -04:00
committed by GitHub
parent b58f29a4ef
commit 2524e6be8b
6 changed files with 66 additions and 103 deletions
+13 -14
View File
@@ -62,31 +62,30 @@ const layer = Layer.effect(
value: decode(row.value),
})
}
const storedRows = (rows: ReadonlyArray<typeof CredentialTable.$inferSelect>) =>
rows.flatMap((row) => {
const credential = stored(row)
return credential ? [credential] : []
})
return Service.of({
all: Effect.fn("Credential.all")(function* () {
return (yield* db
all: Effect.fn("Credential.all")(() =>
db
.select()
.from(CredentialTable)
.orderBy(asc(CredentialTable.time_created))
.all()
.pipe(Effect.orDie)).flatMap((row) => {
const credential = stored(row)
return credential ? [credential] : []
})
}),
list: Effect.fn("Credential.list")(function* (integrationID) {
return (yield* db
.pipe(Effect.orDie, Effect.map(storedRows)),
),
list: Effect.fn("Credential.list")((integrationID) =>
db
.select()
.from(CredentialTable)
.where(eq(CredentialTable.integration_id, integrationID))
.orderBy(asc(CredentialTable.time_created))
.all()
.pipe(Effect.orDie)).flatMap((row) => {
const credential = stored(row)
return credential ? [credential] : []
})
}),
.pipe(Effect.orDie, Effect.map(storedRows)),
),
get: Effect.fn("Credential.get")(function* (id) {
const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
return row ? stored(row) : undefined
+2 -22
View File
@@ -1,5 +1,4 @@
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
import { identity } from "effect/Function"
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient, Statement } from "effect/unstable/sql"
import type { Connection } from "effect/unstable/sql/SqlConnection"
@@ -167,26 +166,7 @@ const make = (options: Config) =>
}),
})
const connection = identity<Connection>({
execute(query, params, transformRows) {
return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params)
},
executeRaw(query, params) {
return run(query, params)
},
executeValues(query, params) {
return runValues(query, params)
},
executeValuesUnprepared(query, params) {
return runValues(query, params)
},
executeUnprepared(query, params, transformRows) {
return this.execute(query, params, transformRows)
},
executeStream() {
return Stream.die("executeStream not implemented")
},
})
const connection = Sqlite.makeConnection(run, runValues, {})
const semaphore = yield* Semaphore.make(1)
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection))
+8 -7
View File
@@ -34,6 +34,7 @@ import { MCPStdio } from "./stdio.js"
const DEFAULT_STARTUP_TIMEOUT = 30_000
const DEFAULT_CATALOG_TIMEOUT = 30_000
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)))
// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
// only that field so a single bad schema doesn't blank out the whole tool list.
@@ -261,7 +262,7 @@ export const connect = Effect.fnUntraced(function* (
},
(result) => result.tools,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) => Effect.logWarning("failed to list MCP tools", { server, error: error.message })),
)
@@ -286,7 +287,7 @@ export const connect = Effect.fnUntraced(function* (
},
(result) => result.prompts,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
@@ -312,7 +313,7 @@ export const connect = Effect.fnUntraced(function* (
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
(result) => result.resources,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
@@ -337,7 +338,7 @@ export const connect = Effect.fnUntraced(function* (
}),
(result) => result.resourceTemplates,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
@@ -355,7 +356,7 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.resources) return undefined
const result = yield* Effect.tryPromise({
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
@@ -378,7 +379,7 @@ export const connect = Effect.fnUntraced(function* (
GetPromptResultSchema,
{ signal, timeout: executionTimeout },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.map((result) => ({
messages: result.messages.map((message) => ({ role: message.role, content: message.content })),
@@ -393,7 +394,7 @@ export const connect = Effect.fnUntraced(function* (
// Keep progress tokens available while enforcing a hard wall-clock execution timeout.
{ signal, timeout: executionTimeout, onprogress: () => {} },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.map((result) => ({
isError: result.isError === true,
+2 -12
View File
@@ -3,6 +3,7 @@ export * as Provider from "./provider.js"
import { Effect, Schema } from "effect"
import { Provider } from "@opencode-ai/schema/provider"
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
import { isRecord } from "@opencode-ai/ai/utils/record"
import { Npm } from "@opencode-ai/util/npm"
import type { DeepMutable } from "./schema.js"
import { importModule, resolveModule } from "@opencode-ai/util/runtime-import"
@@ -108,18 +109,7 @@ export function mergeOverlay(
const left = base[key]
const right = overlay[key]
if (right === undefined) return [key, left]
if (
typeof left === "object" &&
left !== null &&
!Array.isArray(left) &&
typeof right === "object" &&
right !== null &&
!Array.isArray(right)
)
return [
key,
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
]
if (isRecord(left) && isRecord(right)) return [key, mergeOverlay(left, right) ?? {}]
return [key, right]
}),
),
+14 -21
View File
@@ -88,6 +88,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ri
const failure = (message: string, cause?: unknown) => new Error({ message, cause })
const normalizePath = (value: string) =>
value
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
const isInvalidPattern = (stderr: string) =>
stderr.includes("regex parse error") || stderr.includes("error parsing regex")
@@ -169,13 +175,7 @@ const layer = Layer.effect(
"--glob=!**/.git/**",
".",
],
parse: (line) =>
Effect.succeed(
line
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/"),
),
parse: (line) => Effect.succeed(normalizePath(line)),
}).pipe(
Effect.map((result) =>
result.items.map((relative) =>
@@ -203,10 +203,7 @@ const layer = Layer.effect(
".",
],
parse: (line) => {
const relative = line
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
const relative = normalizePath(line)
return Effect.succeed(
Entry.make({
path: RelativePath.make(relative),
@@ -242,7 +239,7 @@ const layer = Layer.effect(
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
Effect.map((match) => ({
...match.data,
path: { text: match.data.path.text.replace(/^\.[\\/]/, "") },
path: { text: normalizePath(match.data.path.text) },
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
})),
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
@@ -251,14 +248,10 @@ const layer = Layer.effect(
),
}).pipe(
Effect.map((result) =>
result.items.map((match) => {
const relative = match.path.text
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
return Match.make({
result.items.map((match) =>
Match.make({
entry: Entry.make({
path: RelativePath.make(relative),
path: RelativePath.make(match.path.text),
type: "file",
}),
line: match.line_number,
@@ -269,8 +262,8 @@ const layer = Layer.effect(
start: submatch.start,
end: submatch.end,
})),
})
}),
}),
),
),
),
})
+27 -27
View File
@@ -180,33 +180,33 @@ const layer = Layer.effect(
.get()
.pipe(Effect.orDie)
}),
create: Effect.fnUntraced(function* (input: StoredInput, tx?: Transaction) {
return (
(yield* (tx ?? db)
.insert(WorktreeTable)
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
.onConflictDoUpdate({
target: [WorktreeTable.project_id, WorktreeTable.directory],
set: { strategy: input.strategy ?? null },
setWhere: input.strategy
? or(isNull(WorktreeTable.strategy), ne(WorktreeTable.strategy, input.strategy))
: isNotNull(WorktreeTable.strategy),
})
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(Effect.orDie)) !== undefined
)
}),
remove: Effect.fnUntraced(function* (projectID: ProjectSchema.ID, directory: AbsolutePath, tx?: Transaction) {
return (
(yield* (tx ?? db)
.delete(WorktreeTable)
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(Effect.orDie)) !== undefined
)
}),
create: (input: StoredInput, tx?: Transaction) =>
(tx ?? db)
.insert(WorktreeTable)
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
.onConflictDoUpdate({
target: [WorktreeTable.project_id, WorktreeTable.directory],
set: { strategy: input.strategy ?? null },
setWhere: input.strategy
? or(isNull(WorktreeTable.strategy), ne(WorktreeTable.strategy, input.strategy))
: isNotNull(WorktreeTable.strategy),
})
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(
Effect.orDie,
Effect.map((row) => row !== undefined),
),
remove: (projectID: ProjectSchema.ID, directory: AbsolutePath, tx?: Transaction) =>
(tx ?? db)
.delete(WorktreeTable)
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(
Effect.orDie,
Effect.map((row) => row !== undefined),
),
}
const registry = new Map<StrategyID, Strategy>()