chore: generate

This commit is contained in:
opencode-agent[bot]
2026-05-31 01:09:55 +00:00
parent 7f571d36ea
commit 102c8353e0
71 changed files with 11252 additions and 8889 deletions
@@ -2,10 +2,7 @@
"version": "7",
"dialect": "sqlite",
"id": "bf93c73b-5a48-4d63-9909-3c36a79b9788",
"prevIds": [
"be5eae31-b7f8-4292-8827-c36a524abd1b",
"fdfcccee-fb3a-481f-b801-b9835fa30d5d"
],
"prevIds": ["be5eae31-b7f8-4292-8827-c36a524abd1b", "fdfcccee-fb3a-481f-b801-b9835fa30d5d"],
"ddl": [
{
"name": "workspace",
@@ -1188,13 +1185,9 @@
"table": "session_share"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1203,13 +1196,9 @@
"table": "workspace"
},
{
"columns": [
"active_account_id"
],
"columns": ["active_account_id"],
"tableTo": "account",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
@@ -1218,13 +1207,9 @@
"table": "account_state"
},
{
"columns": [
"aggregate_id"
],
"columns": ["aggregate_id"],
"tableTo": "event_sequence",
"columnsTo": [
"aggregate_id"
],
"columnsTo": ["aggregate_id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1233,13 +1218,9 @@
"table": "event"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1248,13 +1229,9 @@
"table": "message"
},
{
"columns": [
"message_id"
],
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1263,13 +1240,9 @@
"table": "part"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1278,13 +1251,9 @@
"table": "permission"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1293,13 +1262,9 @@
"table": "session_message"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1308,13 +1273,9 @@
"table": "session"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1323,13 +1284,9 @@
"table": "todo"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1338,137 +1295,105 @@
"table": "session_share"
},
{
"columns": [
"email",
"url"
],
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": [
"session_id",
"position"
],
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": [
"name"
],
"columns": ["name"],
"nameExplicit": false,
"name": "data_migration_pk",
"table": "data_migration",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "account_state_pk",
"table": "account_state",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "account_pk",
"table": "account",
"entityType": "pks"
},
{
"columns": [
"aggregate_id"
],
"columns": ["aggregate_id"],
"nameExplicit": false,
"name": "event_sequence_pk",
"table": "event_sequence",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "event_pk",
"table": "event",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "session_message_pk",
"table": "session_message",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
+10 -3
View File
@@ -25,7 +25,10 @@ const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").sca
for (const name of sqlMigrations) {
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
await Bun.write(path.join(tsDir, `${name}.ts`), renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()))
await Bun.write(
path.join(tsDir, `${name}.ts`),
renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()),
)
}
await Bun.write(registry, renderRegistry(sqlMigrations))
@@ -47,7 +50,9 @@ export default { ...config, out: ${JSON.stringify(output)} }
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
const after = await snapshot(output)
if (JSON.stringify(after) !== JSON.stringify(before)) {
throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.")
throw new Error(
"Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.",
)
}
const migrations = before
@@ -56,7 +61,9 @@ export default { ...config, out: ${JSON.stringify(output)} }
.sort()
for (const name of migrations) {
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`)
throw new Error(
`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`,
)
}
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
+1 -3
View File
@@ -170,6 +170,4 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
)
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
+4 -2
View File
@@ -1,6 +1,7 @@
import type { DatabaseMigration } from "./migration"
export const migrations = (await Promise.all([
export const migrations = (
await Promise.all([
import("./migration/20260127222353_familiar_lady_ursula"),
import("./migration/20260211171708_add_project_commands"),
import("./migration/20260213144116_wakeful_the_professor"),
@@ -22,4 +23,5 @@ export const migrations = (await Promise.all([
import("./migration/20260510033149_session_usage"),
import("./migration/20260511000411_data_migration_state"),
import("./migration/20260530232709_lovely_romulus"),
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -6,7 +6,9 @@ export default {
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`)
yield* tx.run(`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`)
yield* tx.run(
`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`,
)
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
})
},
@@ -7,7 +7,9 @@ export default {
return Effect.gen(function* () {
yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`)
yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`)
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
yield* tx.run(
`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`,
)
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
})
},
@@ -18,7 +18,9 @@ export default {
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`)
yield* tx.run(
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
)
yield* tx.run(`DROP TABLE \`workspace\`;`)
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
yield* tx.run(`PRAGMA foreign_keys=ON;`)
@@ -20,7 +20,9 @@ export default {
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`)
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`)
yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`)
yield* tx.run(`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`)
yield* tx.run(
`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`,
)
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
yield* tx.run(`DROP TABLE \`session_entry\`;`)
})
+11 -6
View File
@@ -49,7 +49,9 @@ const make = (options: Config) =>
const native = (yield* Sqlite.Native) as Database
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
const transformRows = options.transformResultNames
? Statement.defaultTransforms(options.transformResultNames).array
: undefined
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
@@ -102,7 +104,9 @@ const make = (options: Config) =>
export: Effect.try({
try: () => native.serialize(),
catch: (cause) =>
new SqlError({ reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }) }),
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }),
}),
}),
loadExtension: (path) =>
Effect.try({
@@ -119,7 +123,10 @@ const make = (options: Config) =>
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
const fiber = Fiber.getCurrent()!
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
return Effect.as(Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection)
return Effect.as(
Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))),
connection,
)
})
const client = Object.assign(
@@ -172,6 +179,4 @@ export const layer = (config: Config) =>
Layer.merge(
nativeLayer(config),
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
).pipe(
Layer.provide(Reactivity.layer),
)
).pipe(Layer.provide(Reactivity.layer))
+11 -6
View File
@@ -49,7 +49,9 @@ const make = (options: Config) =>
const native = (yield* Sqlite.Native) as DatabaseSync
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
const transformRows = options.transformResultNames
? Statement.defaultTransforms(options.transformResultNames).array
: undefined
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
@@ -72,7 +74,9 @@ const make = (options: Config) =>
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
statement.setReturnArrays(true)
try {
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>)
return Effect.succeed(
statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>,
)
} catch (cause) {
return Effect.fail(
new SqlError({
@@ -113,7 +117,10 @@ const make = (options: Config) =>
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
const fiber = Fiber.getCurrent()!
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
return Effect.as(Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection)
return Effect.as(
Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))),
connection,
)
})
const client = Object.assign(
@@ -167,6 +174,4 @@ export const layer = (config: Config) =>
Layer.merge(
nativeLayer(config),
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
).pipe(
Layer.provide(Reactivity.layer),
)
).pipe(Layer.provide(Reactivity.layer))
+2 -1
View File
@@ -261,7 +261,8 @@ export const layer = Layer.effect(
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location = options?.location ??
const location =
options?.location ??
(serviceLocation
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined)
+1 -4
View File
@@ -16,10 +16,7 @@ const locationLayer = Layer.succeed(
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
)
const it = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
),
Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer)),
)
describe("CatalogV2", () => {
+12 -4
View File
@@ -10,14 +10,18 @@ import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
Effect.runPromise(effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped))
Effect.runPromise(
effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
)
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
describe("DatabaseMigration", () => {
if (process.platform === "linux") {
test("declared schema has no ungenerated migrations", async () => {
const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`.quiet().nothrow()
const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`
.quiet()
.nothrow()
expect(result.exitCode, result.stderr.toString()).toBe(0)
expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
}, 30_000)
@@ -70,7 +74,9 @@ describe("DatabaseMigration", () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
@@ -89,7 +95,9 @@ describe("DatabaseMigration", () => {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`)
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
+36 -6
View File
@@ -211,7 +211,12 @@ describe("EventV2", () => {
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
expect(rows).toHaveLength(1)
expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1))
@@ -227,7 +232,12 @@ describe("EventV2", () => {
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
yield* events.publish(SyncMessage, { id: aggregateID, text: "second" })
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
expect(rows.map((row) => row.seq)).toEqual([0, 1])
}),
@@ -240,7 +250,12 @@ describe("EventV2", () => {
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" })
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
expect(rows).toHaveLength(1)
expect(rows[0]?.aggregate_id).toBe(aggregateID)
@@ -284,7 +299,12 @@ describe("EventV2", () => {
aggregateID,
data: { id: aggregateID, text: "replayed" },
})
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
expect(rows).toHaveLength(1)
expect(rows[0]?.aggregate_id).toBe(aggregateID)
@@ -397,7 +417,12 @@ describe("EventV2", () => {
data: { id: aggregateID, text: "four" },
},
])
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
expect(one).toBe(aggregateID)
expect(two).toBe(aggregateID)
@@ -486,7 +511,12 @@ describe("EventV2", () => {
},
{ ownerID: "owner-2" },
)
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
const rows = yield* db
.select()
.from(EventTable)
.where(eq(EventTable.aggregate_id, aggregateID))
.all()
.pipe(Effect.orDie)
const sequence = yield* db
.select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
.from(EventSequenceTable)
+3 -1
View File
@@ -162,5 +162,7 @@ export const make = (
export const layer = (config: SqliteClientConfig): Layer.Layer<SqliteClient | Client.SqlClient> =>
Layer.effectContext(
Effect.map(make(config), (client) => Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client))),
Effect.map(make(config), (client) =>
Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)),
),
).pipe(Layer.provide(Reactivity.layer))
+2 -10
View File
@@ -48,17 +48,9 @@ export const layer = Layer.effect(
effect.pipe(Effect.mapError((cause) => new AccountRepoError({ message: "Database operation failed", cause })))
const current = Effect.fnUntraced(function* () {
const state = yield* db
.select()
.from(AccountStateTable)
.where(eq(AccountStateTable.id, ACCOUNT_STATE_ID))
.get()
const state = yield* db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
if (!state?.active_account_id) return
const account = yield* db
.select()
.from(AccountTable)
.where(eq(AccountTable.id, state.active_account_id))
.get()
const account = yield* db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
if (!account) return
return { ...account, active_org_id: state.active_org_id ?? null }
})
+6 -1
View File
@@ -148,7 +148,12 @@ export const layer = Layer.effect(
const { db } = yield* Database.Service
const state = yield* InstanceState.make<State>(
Effect.fn("Permission.state")(function* (ctx) {
const row = yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, ctx.project.id))
.get()
.pipe(Effect.orDie)
const state = {
pending: new Map<PermissionID, PendingEntry>(),
approved: [...(row?.data ?? [])],
+28 -6
View File
@@ -201,8 +201,16 @@ export const layer = Layer.effect(
.run()
}
const oldPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
const newPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
const oldPermission = yield* d
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, oldID))
.get()
const newPermission = yield* d
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, newID))
.get()
if (oldPermission && newPermission) {
yield* d
.update(PermissionTable)
@@ -216,7 +224,11 @@ export const layer = Layer.effect(
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
}
if (oldPermission && !newPermission) {
yield* d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
yield* d
.update(PermissionTable)
.set({ project_id: newID })
.where(eq(PermissionTable.project_id, oldID))
.run()
}
yield* d
@@ -224,7 +236,11 @@ export const layer = Layer.effect(
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
.where(eq(SessionTable.project_id, oldID))
.run()
yield* d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
yield* d
.update(WorkspaceTable)
.set({ project_id: newID })
.where(eq(WorkspaceTable.project_id, oldID))
.run()
if (oldProject) yield* d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
}),
@@ -393,13 +409,19 @@ export const layer = Layer.effect(
})
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
yield* db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run().pipe(Effect.orDie)
yield* db
.update(ProjectTable)
.set({ time_initialized: Date.now() })
.where(eq(ProjectTable.id, id))
.run()
.pipe(Effect.orDie)
})
const initState = yield* InstanceState.make(
Effect.fn("Project.initState")(function* (ctx) {
const unsubscribe = yield* events.listen((event) => {
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory) return Effect.void
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory)
return Effect.void
const data = event.data as EventV2.Data<typeof Command.Event.Executed>
return data.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void
})
+3 -5
View File
@@ -328,7 +328,8 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
const unsubscribe = yield* events.listen((event) => {
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory) return Effect.void
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory)
return Effect.void
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
if (!data.file.endsWith("HEAD")) return Effect.void
return Effect.gen(function* () {
@@ -429,9 +430,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
)
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export * as Vcs from "./vcs"
+3 -1
View File
@@ -184,7 +184,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderV2.ID } & CallbackInput) {
const callback = Effect.fn("ProviderAuth.callback")(function* (
input: { providerID: ProviderV2.ID } & CallbackInput,
) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* new OauthMissing({ providerID: input.providerID })
+8 -2
View File
@@ -1024,14 +1024,20 @@ export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModels
export interface Interface {
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
readonly getModel: (
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
) => Effect.Effect<Model, ModelNotFoundError>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
readonly closest: (
providerID: ProviderV2.ID,
query: string[],
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
readonly defaultModel: () => Effect.Effect<
{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID },
DefaultModelError
>
}
interface State {
+1 -2
View File
@@ -1,2 +1 @@
export function initProjectors() {
}
export function initProjectors() {}
@@ -19,7 +19,9 @@ export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (han
return true
})
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderV2.ID } }) {
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: {
params: { providerID: ProviderV2.ID }
}) {
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
return true
})
@@ -43,7 +43,10 @@ function eventResponse(events: EventV2.Interface) {
Stream.map((event) => ({ id: event.id, type: event.type, properties: event.data })),
)
const disposed = Stream.callback<{ id: string; type: string; properties: unknown }>((queue) => {
const listener = (event: { directory?: string; payload: { id?: string; type?: string; properties?: unknown } }) => {
const listener = (event: {
directory?: string
payload: { id?: string; type?: string; properties?: unknown }
}) => {
if (event.directory !== instance.directory || event.payload.type !== "server.instance.disposed") return
Queue.offerUnsafe(queue, {
id: event.payload.id ?? eventID(),
@@ -56,7 +59,10 @@ function eventResponse(events: EventV2.Interface) {
() => Effect.sync(() => GlobalBus.off("event", listener)),
)
})
const output = stream.pipe(Stream.merge(disposed, { haltStrategy: "left" }), Stream.takeUntil((event) => event.type === "server.instance.disposed"))
const output = stream.pipe(
Stream.merge(disposed, { haltStrategy: "left" }),
Stream.takeUntil((event) => event.type === "server.instance.disposed"),
)
const heartbeat = Stream.tick("10 seconds").pipe(
Stream.drop(1),
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),
@@ -88,7 +88,8 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler
yield* events.publish(TuiEvent.PromptAppend, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.CommandExecute.type)
yield* events.publish(TuiEvent.CommandExecute, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.ToastShow.type) yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.ToastShow.type)
yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.SessionSelect.type)
yield* events.publish(TuiEvent.SessionSelect, ctx.payload.properties)
return true
+3 -1
View File
@@ -353,7 +353,9 @@ export const layer = Layer.effect(
throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
}
const userMessage = parent.info
const compactionPart = parent.parts.find((part): part is SessionLegacy.CompactionPart => part.type === "compaction")
const compactionPart = parent.parts.find(
(part): part is SessionLegacy.CompactionPart => part.type === "compaction",
)
let messages = input.messages
let replay:
+4 -2
View File
@@ -168,13 +168,15 @@ const live: Layer.Layer<
const id = PermissionID.ascending()
let unsub: EventV2.Unsubscribe | undefined
try {
unsub = await bridge.promise(events.listen((event) => {
unsub = await bridge.promise(
events.listen((event) => {
if (event.type !== Permission.Event.Replied.type) return Effect.void
const data = event.data as EventV2.Data<typeof Permission.Event.Replied>
if (data.requestID !== id) return Effect.void
void data.reply
return Effect.void
}))
}),
)
const toolPatterns = approvalTools.map((t: { name: string; args: string }) => {
try {
const parsed = JSON.parse(t.args) as Record<string, unknown>
+4 -4
View File
@@ -1502,11 +1502,11 @@ export const layer = Layer.effect(
},
)
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
input: LoopInput,
) {
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(
function* (input: LoopInput) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
})
},
)
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
"SessionPrompt.shell",
+1 -5
View File
@@ -536,11 +536,7 @@ export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert"
export const layer: Layer.Layer<
Service,
never,
| BackgroundJob.Service
| Storage.Service
| RuntimeFlags.Service
| Database.Service
| EventV2Bridge.Service
BackgroundJob.Service | Storage.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
+3 -1
View File
@@ -79,7 +79,9 @@ export const layer = Layer.effect(
const storage = yield* Storage.Service
const events = yield* EventV2Bridge.Service
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: SessionLegacy.WithParts[] }) {
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
messages: SessionLegacy.WithParts[]
}) {
let from: string | undefined
let to: string | undefined
for (const item of input.messages) {
+3 -1
View File
@@ -173,7 +173,9 @@ export const layer = Layer.effect(
events.listen((event) => {
if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
return fn(event.data as EventV2.Data<D>).pipe(
Effect.catchCause((cause) => Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause }))),
Effect.catchCause((cause) =>
Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause })),
),
)
})
+5 -1
View File
@@ -232,7 +232,11 @@ const discoverSkills = Effect.fnUntraced(function* (
}
})
const loadSkills = Effect.fnUntraced(function* (state: State, discovered: DiscoveryState, events: EventV2Bridge.Service["Service"]) {
const loadSkills = Effect.fnUntraced(function* (
state: State,
discovered: DiscoveryState,
events: EventV2Bridge.Service["Service"],
) {
yield* Effect.forEach(discovered.matches, (match) => add(state, match, events), {
concurrency: "unbounded",
discard: true,
+5 -1
View File
@@ -76,7 +76,11 @@ export interface Interface {
readonly ids: () => Effect.Effect<string[]>
readonly all: () => Effect.Effect<Tool.Def[]>
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
readonly tools: (model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
readonly tools: (model: {
providerID: ProviderV2.ID
modelID: ProviderV2.ModelID
agent: Agent.Info
}) => Effect.Effect<Tool.Def[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
+13 -2
View File
@@ -149,7 +149,13 @@ type GitResult = { code: number; text: string; stderr: string }
export const layer: Layer.Layer<
Service,
never,
AppFileSystem.Service | Path.Path | AppProcess.Service | Git.Service | Project.Service | InstanceStore.Service | Database.Service
| AppFileSystem.Service
| Path.Path
| AppProcess.Service
| Git.Service
| Project.Service
| InstanceStore.Service
| Database.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -484,7 +490,12 @@ export const layer: Layer.Layer<
directory: string,
input: { projectID: ProjectV2.ID; extra?: string },
) {
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(ProjectTable)
.where(eq(ProjectTable.id, input.projectID))
.get()
.pipe(Effect.orDie)
const project = row ? Project.fromRow(row) : undefined
const startup = project?.commands?.start?.trim() ?? ""
const ok = yield* runStartScript(directory, startup, "project")
@@ -26,7 +26,11 @@ function createReasoningPart(text: string): SessionLegacy.Part {
}
}
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): SessionLegacy.Part {
function createToolPart(
tool: string,
title: string,
status: "completed" | "running" = "completed",
): SessionLegacy.Part {
if (status === "completed") {
return {
id: PartID.ascending(),
+3 -1
View File
@@ -105,7 +105,9 @@ describe("Format", () => {
{ config: { formatter: false } },
)
testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer)).live("status() initializes formatter state per directory", () =>
testEffect(
Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
).live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.use.status(), {
config: { formatter: false },
+2 -8
View File
@@ -19,17 +19,11 @@ const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
const experimentalTyIt = testEffect(
Layer.mergeAll(
lspLayer({ experimentalLspTy: true }),
CrossSpawnSpawner.defaultLayer,
),
Layer.mergeAll(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
)
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
const disabledDownloadIt = testEffect(
Layer.mergeAll(
lspLayer({ disableLspDownload: true }),
CrossSpawnSpawner.defaultLayer,
),
Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
)
describe("lsp.spawn", () => {
@@ -657,7 +657,8 @@ it.instance(
const events = yield* EventV2Bridge.Service
const seen = yield* Deferred.make<Permission.Request>()
const unsub = yield* events.listen((event) => {
if (event.type === Permission.Event.Asked.type) Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
if (event.type === Permission.Event.Asked.type)
Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@@ -932,7 +933,10 @@ it.instance(
const unsub = yield* events.listen((event) => {
if (event.type === Permission.Event.Replied.type)
Deferred.doneUnsafe(seen, Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }))
Deferred.doneUnsafe(
seen,
Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@@ -20,7 +20,9 @@ afterEach(async () => {
await disposeAllInstances()
})
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
const it = testEffect(
Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer),
)
function withTmp<T, A, E, R>(
init: (dir: string) => Promise<T>,
+19 -4
View File
@@ -238,10 +238,25 @@ describe("Project.fromDirectory", () => {
const result = yield* projects.fromDirectory(tmp)
expect(result.project.id).toBe(remoteID)
expect(yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie)).toBeUndefined()
expect((yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
expect(yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get().pipe(Effect.orDie)).toBeDefined()
expect((yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
expect(
yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie),
).toBeUndefined()
expect(
(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))
?.project_id,
).toBe(remoteID)
expect(
yield* db
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, remoteID))
.get()
.pipe(Effect.orDie),
).toBeDefined()
expect(
(yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))
?.project_id,
).toBe(remoteID)
}),
)
})
+7 -1
View File
@@ -5,7 +5,13 @@ import { Deferred, Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import fs from "fs/promises"
import path from "path"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { FileWatcher } from "../../src/file/watcher"
import { Git } from "../../src/git"
@@ -180,7 +180,9 @@ it.instance(
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(
providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"],
).toBeDefined()
}),
{
config: {
@@ -245,9 +245,9 @@ it.instance(
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
expect(providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
false,
)
expect(
providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
).toBe(false)
}),
{
config: {
@@ -305,7 +305,9 @@ it.instance("getModel returns model for valid provider/model", () =>
it.instance("getModel throws ModelNotFoundError for invalid model", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const exit = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model")).pipe(Effect.exit)
const exit = yield* Provider.use
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model"))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
)
@@ -977,8 +979,14 @@ it.instance(
it.instance("getModel returns consistent results", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
const model1 = yield* Provider.use.getModel(
ProviderV2.ID.anthropic,
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
)
const model2 = yield* Provider.use.getModel(
ProviderV2.ID.anthropic,
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
)
expect(model1.providerID).toEqual(model2.providerID)
expect(model1.id).toEqual(model2.id)
expect(model1).toEqual(model2)
@@ -1008,7 +1016,9 @@ it.instance(
it.instance("ModelNotFoundError includes suggestions for typos", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const error = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4")).pipe(Effect.flip)
const error = yield* Provider.use
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4"))
.pipe(Effect.flip)
expect(error.suggestions).toBeDefined()
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
}),
@@ -1565,7 +1575,10 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "eu")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(
ProviderV2.ID.make("google-vertex"),
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
)
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models",
@@ -1594,7 +1607,10 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "europe-west1")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(
ProviderV2.ID.make("google-vertex"),
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
)
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models",
+3 -1
View File
@@ -50,7 +50,9 @@ describe("PTY websocket tickets", () => {
const workspaceID = WorkspaceV2.ID.ascending()
const issued = yield* tickets.issue({ ptyID, workspaceID })
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(false)
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(
false,
)
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
}),
)
@@ -14,7 +14,11 @@ const it = testEffect(
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
)
const lifecycle = testEffect(
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer),
Layer.mergeAll(
Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
CrossSpawnSpawner.defaultLayer,
testInstanceStoreLayer,
),
)
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
@@ -94,5 +94,4 @@ describe("event HttpApi", () => {
}),
{ git: true, config: { formatter: false, lsp: false } },
)
})
@@ -170,7 +170,11 @@ const insertRemoteWorkspaceWithoutSync = (input: {
const id = WorkspaceV2.ID.ascending()
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
const { db } = yield* Database.Service
yield* db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run().pipe(Effect.orDie)
yield* db
.insert(WorkspaceTable)
.values({ id, type: input.type, project_id: input.projectID })
.run()
.pipe(Effect.orDie)
return id
})
@@ -331,7 +335,9 @@ describe("HttpApi workspace routing middleware", () => {
const project = yield* Project.use.fromDirectory(dir)
const workspaceID = WorkspaceV2.ID.ascending()
const type = "remote-http-fence-target"
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(undefined)
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(
undefined,
)
const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
HttpServerResponse.json(
@@ -154,8 +154,18 @@ describe("session.list", () => {
)
const { db } = yield* Database.Service
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run().pipe(Effect.orDie)
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run().pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ path: null })
.where(eq(SessionTable.id, current.id))
.run()
.pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ path: null })
.where(eq(SessionTable.id, sibling.id))
.run()
.pipe(Effect.orDie)
const pathIDs = (yield* SessionNs.Service.use((session) =>
session.list({
@@ -247,7 +247,12 @@ const env = Layer.mergeAll(
const it = testEffect(env)
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer, EventV2Bridge.defaultLayer, CrossSpawnSpawner.defaultLayer)
const compactionEnv = Layer.mergeAll(
SessionNs.defaultLayer,
Database.defaultLayer,
EventV2Bridge.defaultLayer,
CrossSpawnSpawner.defaultLayer,
)
const itCompaction = testEffect(compactionEnv)
type CompactionProcessOptions = {
@@ -587,7 +592,6 @@ describe("session.compaction.create", () => {
auto: true,
overflow: true,
})
}),
),
)
@@ -852,7 +856,8 @@ describe("session.compaction.process", () => {
let seen = false
const unsub = yield* events.listen((evt) => {
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id) return Effect.void
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id)
return Effect.void
seen = true
Deferred.doneUnsafe(done, Effect.void)
return Effect.void
+8 -2
View File
@@ -1617,7 +1617,10 @@ describe("session.llm.stream", () => {
]
const request = waitRequest("/messages", createEventResponse(chunks))
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ProviderV2.ModelID.make(model.id))
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make("anthropic"),
ProviderV2.ModelID.make(model.id),
)
const sessionID = SessionID.make("session-test-anthropic-tools")
const agent = {
name: "test",
@@ -1816,7 +1819,10 @@ describe("session.llm.stream", () => {
]
const request = waitRequest(pathSuffix, createEventResponse(chunks))
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make(geminiFixture.providerID), ProviderV2.ModelID.make(model.id))
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make(geminiFixture.providerID),
ProviderV2.ModelID.make(model.id),
)
const sessionID = SessionID.make("session-test-4")
const agent = {
name: "test",
@@ -991,7 +991,9 @@ describe("session.message-v2.toModelMessage", () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
const aborted = new SessionLegacy.AbortedError({ message: "aborted" }).toObject() as SessionLegacy.Assistant["error"]
const aborted = new SessionLegacy.AbortedError({
message: "aborted",
}).toObject() as SessionLegacy.Assistant["error"]
const input: SessionLegacy.WithParts[] = [
{
@@ -539,7 +539,12 @@ noLLMServer.instance.skip(
Effect.provide(SessionV2.defaultLayer),
)
const { db } = yield* Database.Service
const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.session_id, chat.id)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.session_id, chat.id))
.get()
.pipe(Effect.orDie)
expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
expect(typeof row?.data.time.created).toBe("number")
expect(messages).toEqual(
@@ -49,7 +49,10 @@ describe("session.created event", () => {
const unsub = yield* events.listen((event) => {
if (event.type === SessionNs.Event.Created.type)
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info))
Deferred.doneUnsafe(
received,
Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@@ -127,7 +130,10 @@ describe("step-finish token propagation via event", () => {
const received = yield* Deferred.make<SessionLegacy.Part>()
const unsub = yield* events.listen((event) => {
if (event.type === MessageV2.Event.PartUpdated.type)
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part))
Deferred.doneUnsafe(
received,
Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@@ -75,7 +75,12 @@ function wired(client: HttpClient.HttpClient) {
const share = (id: SessionID) =>
Effect.gen(function* () {
const { db } = yield* Database.Service
return yield* db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, id)).get().pipe(Effect.orDie)
return yield* db
.select()
.from(SessionShareTable)
.where(eq(SessionShareTable.session_id, id))
.get()
.pipe(Effect.orDie)
})
const seed = (url: string, org?: string) =>
@@ -6,7 +6,13 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Fiber, Layer } from "effect"
import { Snapshot } from "../../src/snapshot"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
@@ -17,7 +17,10 @@ function migrations() {
.map((entry) => ({
name: entry.name,
timestamp: Number(entry.name.split("_")[0]),
sql: readFileSync(path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"), "utf-8"),
sql: readFileSync(
path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"),
"utf-8",
),
}))
.sort((a, b) => a.timestamp - b.timestamp)
}
+7 -1
View File
@@ -15,7 +15,13 @@ import { ReadTool } from "../../src/tool/read"
import { Truncate } from "@/tool/truncate"
import { Tool } from "@/tool/tool"
import { Filesystem } from "@/util/filesystem"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
@@ -12,7 +12,8 @@ test.skip("step snapshots carry over to assistant messages", () => {
const state: SessionMessageUpdater.MemoryState = { messages: [] }
const sessionID = SessionID.make("session")
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
@@ -26,9 +27,11 @@ test.skip("step snapshots carry over to assistant messages", () => {
},
snapshot: "before",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.ended",
data: {
@@ -44,7 +47,8 @@ test.skip("step snapshots carry over to assistant messages", () => {
},
snapshot: "after",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@@ -56,7 +60,8 @@ test.skip("text ended populates assistant text content", () => {
const state: SessionMessageUpdater.MemoryState = { messages: [] }
const sessionID = SessionID.make("session")
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
@@ -69,18 +74,22 @@ test.skip("text ended populates assistant text content", () => {
variant: ModelV2.VariantID.make("default"),
},
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.ended",
data: {
@@ -88,7 +97,8 @@ test.skip("text ended populates assistant text content", () => {
timestamp: DateTime.makeUnsafe(3),
text: "hello assistant",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@@ -100,7 +110,8 @@ test.skip("tool completion stores completed timestamp", () => {
const sessionID = SessionID.make("session")
const callID = "call"
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
@@ -113,9 +124,11 @@ test.skip("tool completion stores completed timestamp", () => {
variant: ModelV2.VariantID.make("default"),
},
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.input.started",
data: {
@@ -124,9 +137,11 @@ test.skip("tool completion stores completed timestamp", () => {
callID,
name: "bash",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.called",
data: {
@@ -137,9 +152,11 @@ test.skip("tool completion stores completed timestamp", () => {
input: { command: "pwd" },
provider: { executed: true, metadata: { source: "provider" } },
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.success",
data: {
@@ -150,7 +167,8 @@ test.skip("tool completion stores completed timestamp", () => {
content: [{ type: "text", text: "/tmp" }],
provider: { executed: true, metadata: { status: "done" } },
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@@ -165,7 +183,8 @@ test.skip("compaction events reduce to compaction message", () => {
const sessionID = SessionID.make("session")
const id = EventV2.ID.create()
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id,
type: "session.next.compaction.started",
data: {
@@ -173,9 +192,11 @@ test.skip("compaction events reduce to compaction message", () => {
timestamp: DateTime.makeUnsafe(1),
reason: "auto",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
@@ -183,9 +204,11 @@ test.skip("compaction events reduce to compaction message", () => {
timestamp: DateTime.makeUnsafe(2),
text: "hello ",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
@@ -193,9 +216,11 @@ test.skip("compaction events reduce to compaction message", () => {
timestamp: DateTime.makeUnsafe(3),
text: "summary",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.ended",
data: {
@@ -204,7 +229,8 @@ test.skip("compaction events reduce to compaction message", () => {
text: "final summary",
include: "recent context",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
expect(state.messages).toHaveLength(1)
expect(state.messages[0]).toMatchObject({
+5 -5
View File
@@ -25,10 +25,10 @@ import type {
ConfigUpdateErrors,
ConfigUpdateResponses,
EventSubscribeResponses,
EventTuiCommandExecute2,
EventTuiPromptAppend2,
EventTuiSessionSelect2,
EventTuiToastShow2,
EventTuiCommandExecute,
EventTuiPromptAppend,
EventTuiSessionSelect,
EventTuiToastShow,
ExperimentalConsoleGetErrors,
ExperimentalConsoleGetResponses,
ExperimentalConsoleListOrgsErrors,
@@ -4950,7 +4950,7 @@ export class Tui extends HeyApiClient {
parameters?: {
directory?: string
workspace?: string
body?: EventTuiPromptAppend2 | EventTuiCommandExecute2 | EventTuiToastShow2 | EventTuiSessionSelect2
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
},
options?: Options<never, ThrowOnError>,
) {
File diff suppressed because it is too large Load Diff
+7132 -5519
View File
File diff suppressed because it is too large Load Diff