mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab31c41fee |
@@ -154,10 +154,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
resetHeartbeat()
|
||||
streamErrorLogged = false
|
||||
const directory = event.directory ?? "global"
|
||||
if (event.payload.type === "sync") {
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = event.payload as Event
|
||||
|
||||
const k = key(directory, payload)
|
||||
|
||||
@@ -17,6 +17,17 @@ export function define<Type extends string, Properties extends Schema.Top>(
|
||||
return result
|
||||
}
|
||||
|
||||
// Optional source-of-truth metadata for event-sourcing replay. Present on
|
||||
// GlobalBus events that originated from `SyncEvent.run`; absent on transient
|
||||
// bus events that don't have an event log entry. Consumers that replay the
|
||||
// event log (cross-instance sync) filter by `payload.sync != null`.
|
||||
const Sync = Schema.Struct({
|
||||
name: Schema.String,
|
||||
seq: Schema.Finite,
|
||||
aggregateID: Schema.String,
|
||||
data: Schema.Unknown,
|
||||
}).annotate({ identifier: "Event.Sync" })
|
||||
|
||||
export function effectPayloads() {
|
||||
return [
|
||||
...registry
|
||||
@@ -26,6 +37,7 @@ export function effectPayloads() {
|
||||
id: Schema.String,
|
||||
type: Schema.Literal(type),
|
||||
properties: def.properties,
|
||||
sync: Schema.optional(Sync),
|
||||
}).annotate({ identifier: `Event.${type}` }),
|
||||
)
|
||||
.toArray(),
|
||||
@@ -36,6 +48,7 @@ export function effectPayloads() {
|
||||
id: Schema.String,
|
||||
type: Schema.Literal(definition.type),
|
||||
properties: definition.data,
|
||||
sync: Schema.optional(Sync),
|
||||
}).annotate({ identifier: `Event.${definition.type}` }),
|
||||
)
|
||||
.toArray(),
|
||||
|
||||
@@ -13,7 +13,7 @@ class GlobalBusEmitter extends EventEmitter<{
|
||||
}> {
|
||||
override emit(eventName: "event", event: GlobalEvent): boolean {
|
||||
if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) {
|
||||
event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending")
|
||||
event.payload.id = Identifier.create("evt", "ascending")
|
||||
}
|
||||
return super.emit(eventName, event)
|
||||
}
|
||||
|
||||
@@ -31,11 +31,21 @@ type State = {
|
||||
typed: Map<string, PubSub.PubSub<Payload>>
|
||||
}
|
||||
|
||||
export type SyncMetadata = {
|
||||
readonly name: string
|
||||
readonly seq: number
|
||||
readonly aggregateID: string
|
||||
readonly data: unknown
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
// `sync` carries event-sourcing metadata when the publish originates from
|
||||
// `SyncEvent.run`/`replay`. It rides on the same GlobalBus event as the
|
||||
// projection — one wire event per domain event, with both views inside.
|
||||
readonly publish: <D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: BusProperties<D>,
|
||||
options?: { id?: string },
|
||||
options?: { id?: string; sync?: SyncMetadata },
|
||||
) => Effect.Effect<void>
|
||||
// subscribe / subscribeAll are eager: the underlying PubSub subscription is
|
||||
// acquired in the caller's Scope at `yield*` time. Any publish after the
|
||||
@@ -94,7 +104,11 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>, options?: { id?: string }) {
|
||||
function publish<D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: BusProperties<D>,
|
||||
options?: { id?: string; sync?: SyncMetadata },
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties }
|
||||
@@ -112,7 +126,7 @@ export const layer = Layer.effect(
|
||||
directory: dir,
|
||||
project: context.project.id,
|
||||
workspace,
|
||||
payload,
|
||||
payload: options?.sync ? { ...payload, sync: options.sync } : payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,12 +170,7 @@ function globalPayloadEvent(value: unknown): Event | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payload = value.payload
|
||||
if (payload.type === "sync") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return isEvent(payload) ? payload : undefined
|
||||
return isEvent(value.payload) ? value.payload : undefined
|
||||
}
|
||||
|
||||
function isMatchingDisposeEvent(value: unknown, directory: string | undefined): boolean {
|
||||
|
||||
@@ -12,10 +12,6 @@ export function useEvent() {
|
||||
|
||||
function subscribe(handler: (event: Event, metadata: EventMetadata) => void) {
|
||||
return sdk.event.on("event", (event) => {
|
||||
if (event.payload.type === "sync") {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.directory === "global" || event.project === project.project()) {
|
||||
handler(event.payload, { workspace: event.workspace })
|
||||
}
|
||||
|
||||
@@ -428,11 +428,22 @@ export const layer = Layer.effect(
|
||||
yield* parseSSE(stream, (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (!evt || typeof evt !== "object" || !("payload" in evt)) return
|
||||
const payload = evt.payload as { type?: string; syncEvent?: SyncEvent.SerializedEvent }
|
||||
const payload = evt.payload as {
|
||||
id?: string
|
||||
type?: string
|
||||
sync?: { name: string; seq: number; aggregateID: string; data: unknown }
|
||||
}
|
||||
if (payload.type === "server.heartbeat") return
|
||||
|
||||
if (payload.type === "sync" && payload.syncEvent) {
|
||||
const failed = yield* sync.replay(payload.syncEvent).pipe(
|
||||
if (payload.sync) {
|
||||
const serialized: SyncEvent.SerializedEvent = {
|
||||
id: payload.id ?? "",
|
||||
type: payload.sync.name,
|
||||
seq: payload.sync.seq,
|
||||
aggregateID: payload.sync.aggregateID,
|
||||
data: payload.sync.data as never,
|
||||
}
|
||||
const failed = yield* sync.replay(serialized).pipe(
|
||||
Effect.as(false),
|
||||
Effect.catchCause((error) =>
|
||||
Effect.sync(() => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import "@/server/event"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -15,7 +14,10 @@ const GlobalEventSchema = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
project: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
payload: Schema.Union([...BusEvent.effectPayloads(), ...SyncEvent.effectPayloads()]),
|
||||
// One shape per event. Source-of-truth sync metadata, when present, is
|
||||
// carried as an optional `sync` field inside the payload — see
|
||||
// `BusEvent.effectPayloads()`.
|
||||
payload: Schema.Union(BusEvent.effectPayloads()),
|
||||
}).annotate({ identifier: "GlobalEvent" })
|
||||
|
||||
export const GlobalUpgradeInput = Schema.Struct({
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
// Remove that registry read when event schemas are generated from core directly.
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Bus as ProjectBus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { EventSequenceTable, EventTable } from "./event.sql"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import { EventID } from "./schema"
|
||||
import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
@@ -15,7 +16,7 @@ import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { attachWith } from "@/effect/run-service"
|
||||
|
||||
// Keep `Event["data"]` mutable because projectors mutate the persisted shape
|
||||
// when writing to the database. Bus payloads (`Properties`) stay readonly —
|
||||
@@ -49,6 +50,10 @@ export type SerializedEvent<Def extends Definition = Definition> = Event<Def> &
|
||||
|
||||
type ProjectorFunc = (db: Database.TxOrDb, data: unknown, event: Event) => void
|
||||
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
|
||||
type PublishContext = {
|
||||
instance?: InstanceContext
|
||||
workspace?: WorkspaceID
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly run: <Def extends Definition>(
|
||||
@@ -101,14 +106,16 @@ export const layer = Layer.effect(Service)(
|
||||
}
|
||||
|
||||
const publish = !!options?.publish
|
||||
// Bridge captures handler-fiber refs (InstanceRef/WorkspaceRef) and the
|
||||
// full Effect context, so the forked publish + GlobalBus emit run with
|
||||
// the right state without a per-call attachWith.
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const context = publish
|
||||
? {
|
||||
instance: yield* InstanceState.context,
|
||||
workspace: yield* InstanceState.workspaceID,
|
||||
}
|
||||
: undefined
|
||||
process(def, event, {
|
||||
bus,
|
||||
bridge,
|
||||
publish,
|
||||
context,
|
||||
ownerID: options?.ownerID,
|
||||
experimentalWorkspaces: flags.experimentalWorkspaces,
|
||||
})
|
||||
@@ -146,7 +153,12 @@ export const layer = Layer.effect(Service)(
|
||||
}
|
||||
|
||||
const { publish = true } = options || {}
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const context = publish
|
||||
? {
|
||||
instance: yield* InstanceState.context,
|
||||
workspace: yield* InstanceState.workspaceID,
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Note that this is an "immediate" transaction which is critical.
|
||||
// We need to make sure we can safely read and write with nothing
|
||||
@@ -162,7 +174,7 @@ export const layer = Layer.effect(Service)(
|
||||
const seq = row?.seq != null ? row.seq + 1 : 0
|
||||
|
||||
const event = { id, seq, aggregateID: agg, data }
|
||||
process(def, event, { bus, bridge, publish, experimentalWorkspaces: flags.experimentalWorkspaces })
|
||||
process(def, event, { bus, publish, context, experimentalWorkspaces: flags.experimentalWorkspaces })
|
||||
},
|
||||
{
|
||||
behavior: "immediate",
|
||||
@@ -295,8 +307,8 @@ function process<Def extends Definition>(
|
||||
event: Event<Def>,
|
||||
options: {
|
||||
bus: ProjectBus.Interface
|
||||
bridge: EffectBridge.Shape
|
||||
publish: boolean
|
||||
context?: PublishContext
|
||||
ownerID?: string
|
||||
experimentalWorkspaces: boolean
|
||||
},
|
||||
@@ -338,36 +350,30 @@ function process<Def extends Definition>(
|
||||
}
|
||||
|
||||
Database.effect(() => {
|
||||
if (!options.publish) return
|
||||
const result = convertEvent(def.type, event.data)
|
||||
// The bridge was built inside the caller's fiber so it already carries
|
||||
// InstanceRef/WorkspaceRef and the full Effect context. Both the bus
|
||||
// publish and the GlobalBus emit run inside the forked Effect so they
|
||||
// share the same instance/workspace lookup.
|
||||
const publish = (data: unknown) =>
|
||||
options.bridge.fork(
|
||||
Effect.gen(function* () {
|
||||
yield* options.bus.publish(def, data as Properties<Def>, { id: event.id })
|
||||
const instance = yield* InstanceState.context
|
||||
const workspace = yield* InstanceState.workspaceID
|
||||
GlobalBus.emit("event", {
|
||||
directory: instance.directory,
|
||||
project: instance.project.id,
|
||||
workspace,
|
||||
payload: {
|
||||
type: "sync",
|
||||
syncEvent: {
|
||||
type: versionedType(def.type, def.version),
|
||||
...event,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
if (result instanceof Promise) {
|
||||
void result.then(publish)
|
||||
} else {
|
||||
publish(result)
|
||||
if (options?.publish) {
|
||||
if (!options.context?.instance) {
|
||||
throw new Error("SyncEvent.process: publish requires instance context")
|
||||
}
|
||||
|
||||
const sync: ProjectBus.SyncMetadata = {
|
||||
name: versionedType(def.type, def.version),
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
data: event.data,
|
||||
}
|
||||
const result = convertEvent(def.type, event.data)
|
||||
const publish = (data: unknown) =>
|
||||
Effect.runPromise(
|
||||
attachWith(options.bus.publish(def, data as Properties<Def>, { id: event.id, sync }), {
|
||||
instance: options.context?.instance,
|
||||
workspace: options.context?.workspace,
|
||||
}),
|
||||
)
|
||||
if (result instanceof Promise) {
|
||||
void result.then(publish)
|
||||
} else {
|
||||
void publish(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,12 +29,6 @@ function rememberEnv(k: string) {
|
||||
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
|
||||
}
|
||||
|
||||
const setProcessEnv = (k: string, v: string) =>
|
||||
Effect.sync(() => {
|
||||
rememberEnv(k)
|
||||
process.env[k] = v
|
||||
})
|
||||
|
||||
const set = (ctx: InstanceContext, k: string, v: string) => {
|
||||
rememberEnv(k)
|
||||
process.env[k] = v
|
||||
@@ -134,159 +128,236 @@ const alphaProviderConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
it.instance("provider loaded from env variable", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Provider should retain its connection source even if custom loaders
|
||||
// merge additional options.
|
||||
expect(providers[ProviderID.anthropic].source).toBe("env")
|
||||
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"provider loaded from config with apiKey option",
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
anthropic: {
|
||||
options: {
|
||||
apiKey: "config-api-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
test("provider loaded from env variable", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"disabled_providers excludes provider",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
}),
|
||||
{ config: { disabled_providers: ["anthropic"] } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"enabled_providers restricts to only listed providers",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
yield* setProcessEnv("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
}),
|
||||
{ config: { enabled_providers: ["anthropic"] } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"model whitelist filters models for provider",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
expect(models.length).toBe(1)
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
anthropic: {
|
||||
whitelist: ["claude-sonnet-4-20250514"],
|
||||
},
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Provider should retain its connection source even if custom loaders
|
||||
// merge additional options.
|
||||
expect(providers[ProviderID.anthropic].source).toBe("env")
|
||||
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it.instance(
|
||||
"model blacklist excludes specific models",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).not.toContain("claude-sonnet-4-20250514")
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
anthropic: {
|
||||
blacklist: ["claude-sonnet-4-20250514"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"custom model alias via config",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
anthropic: {
|
||||
models: {
|
||||
"my-alias": {
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "My Custom Alias",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"custom provider with npm package",
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
expect(providers[ProviderID.make("custom-provider")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider")
|
||||
expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined()
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
"custom-provider": {
|
||||
name: "Custom Provider",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.custom.com/v1",
|
||||
env: ["CUSTOM_API_KEY"],
|
||||
models: {
|
||||
"custom-model": {
|
||||
name: "Custom Model",
|
||||
tool_call: true,
|
||||
limit: {
|
||||
context: 128000,
|
||||
output: 4096,
|
||||
test("provider loaded from config with apiKey option", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
anthropic: {
|
||||
options: {
|
||||
apiKey: "config-api-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("disabled_providers excludes provider", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
disabled_providers: ["anthropic"],
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("enabled_providers restricts to only listed providers", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: ["anthropic"],
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
await set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("model whitelist filters models for provider", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
anthropic: {
|
||||
whitelist: ["claude-sonnet-4-20250514"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
expect(models.length).toBe(1)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("model blacklist excludes specific models", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
anthropic: {
|
||||
blacklist: ["claude-sonnet-4-20250514"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).not.toContain("claude-sonnet-4-20250514")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("custom model alias via config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
anthropic: {
|
||||
models: {
|
||||
"my-alias": {
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "My Custom Alias",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("custom provider with npm package", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"custom-provider": {
|
||||
name: "Custom Provider",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.custom.com/v1",
|
||||
env: ["CUSTOM_API_KEY"],
|
||||
models: {
|
||||
"custom-model": {
|
||||
name: "Custom Model",
|
||||
tool_call: true,
|
||||
limit: {
|
||||
context: 128000,
|
||||
output: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("custom-provider")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider")
|
||||
expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it.instance(
|
||||
"filters alpha provider models by default",
|
||||
@@ -308,58 +379,66 @@ experimentalModels.instance(
|
||||
{ config: alphaProviderConfig },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"custom DeepSeek openai-compatible model defaults interleaved reasoning field",
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service.use((provider) => provider.list())
|
||||
const provider = providers[ProviderID.make("custom-provider")]
|
||||
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[ProviderID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
|
||||
false,
|
||||
)
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
"custom-provider": {
|
||||
name: "Custom Provider",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.custom.com/v1",
|
||||
models: {
|
||||
"deepseek-r1": {
|
||||
name: "DeepSeek R1",
|
||||
test("custom DeepSeek openai-compatible model defaults interleaved reasoning field", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"custom-provider": {
|
||||
name: "Custom Provider",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.custom.com/v1",
|
||||
models: {
|
||||
"deepseek-r1": {
|
||||
name: "DeepSeek R1",
|
||||
},
|
||||
"deepseek-details": {
|
||||
name: "DeepSeek Details",
|
||||
interleaved: { field: "reasoning_details" },
|
||||
},
|
||||
"custom-model": {
|
||||
name: "Custom Model",
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
"deepseek-details": {
|
||||
name: "DeepSeek Details",
|
||||
interleaved: { field: "reasoning_details" },
|
||||
},
|
||||
"custom-model": {
|
||||
name: "Custom Model",
|
||||
"custom-anthropic-provider": {
|
||||
name: "Custom Anthropic Provider",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
api: "https://api.custom.com/v1",
|
||||
models: {
|
||||
"deepseek-r1": {
|
||||
name: "DeepSeek R1",
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
"custom-anthropic-provider": {
|
||||
name: "Custom Anthropic Provider",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
api: "https://api.custom.com/v1",
|
||||
models: {
|
||||
"deepseek-r1": {
|
||||
name: "DeepSeek R1",
|
||||
},
|
||||
},
|
||||
options: {
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const provider = providers[ProviderID.make("custom-provider")]
|
||||
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[ProviderID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
|
||||
).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("env variable takes precedence, config merges options", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { describe, expect, beforeEach, afterAll } from "bun:test"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { Deferred, Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { EventSequenceTable, EventTable } from "../../src/sync/event.sql"
|
||||
import { MessageID } from "../../src/session/schema"
|
||||
import { initProjectors } from "../../src/server/projectors"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const it = testEffect(
|
||||
@@ -140,43 +139,6 @@ describe("SyncEvent", () => {
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// Regression for the EffectBridge migration. GlobalBus.emit used to fire
|
||||
// synchronously inside the Database.effect post-commit callback. After the
|
||||
// migration it fires inside the forked publish Effect, AFTER bus.publish
|
||||
// completes. Consumers don't care about microsecond-level ordering, but
|
||||
// we still need to prove the emit actually fires.
|
||||
it.live(
|
||||
"emits sync events to GlobalBus after publishing to ProjectBus",
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const { Created } = setup()
|
||||
// Filter for OUR specific event in the handler so we ignore any
|
||||
// stray sync events from other tests' lingering forks.
|
||||
const received = yield* Deferred.make<GlobalEvent>()
|
||||
const handler = (evt: GlobalEvent) => {
|
||||
if (evt.payload?.type === "sync" && evt.payload?.syncEvent?.type === "item.created.1") {
|
||||
Deferred.doneUnsafe(received, Effect.succeed(evt))
|
||||
}
|
||||
}
|
||||
GlobalBus.on("event", handler)
|
||||
try {
|
||||
yield* SyncEvent.use.run(Created, { id: "evt_global_1", name: "global" })
|
||||
const event = yield* awaitWithTimeout(
|
||||
Deferred.await(received),
|
||||
"timed out waiting for sync event on GlobalBus",
|
||||
"2 seconds",
|
||||
)
|
||||
expect(event.payload).toMatchObject({
|
||||
type: "sync",
|
||||
syncEvent: { type: "item.created.1", data: { id: "evt_global_1", name: "global" } },
|
||||
})
|
||||
} finally {
|
||||
GlobalBus.off("event", handler)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("replay", () => {
|
||||
|
||||
@@ -5,10 +5,10 @@ export type ClientOptions = {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiPromptAppend1
|
||||
| EventTuiCommandExecute1
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventTuiSessionSelect1
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventServerInstanceDisposed
|
||||
@@ -110,6 +110,7 @@ export type EventTuiPromptAppend = {
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
@@ -135,6 +136,7 @@ export type EventTuiCommandExecute = {
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
@@ -146,6 +148,7 @@ export type EventTuiToastShow = {
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
@@ -157,6 +160,7 @@ export type EventTuiSessionSelect = {
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type PermissionRequest = {
|
||||
@@ -863,39 +867,6 @@ export type GlobalEvent = {
|
||||
| EventSessionNextCompactionDelta
|
||||
| EventSessionNextCompactionEnded
|
||||
| EventCatalogModelUpdated
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
| SyncEventMessagePartUpdated
|
||||
| SyncEventMessagePartRemoved
|
||||
| SyncEventSessionCreated
|
||||
| SyncEventSessionUpdated
|
||||
| SyncEventSessionDeleted
|
||||
| SyncEventSessionNextAgentSwitched
|
||||
| SyncEventSessionNextModelSwitched
|
||||
| SyncEventSessionNextPrompted
|
||||
| SyncEventSessionNextSynthetic
|
||||
| SyncEventSessionNextShellStarted
|
||||
| SyncEventSessionNextShellEnded
|
||||
| SyncEventSessionNextStepStarted
|
||||
| SyncEventSessionNextStepEnded
|
||||
| SyncEventSessionNextStepFailed
|
||||
| SyncEventSessionNextTextStarted
|
||||
| SyncEventSessionNextTextDelta
|
||||
| SyncEventSessionNextTextEnded
|
||||
| SyncEventSessionNextReasoningStarted
|
||||
| SyncEventSessionNextReasoningDelta
|
||||
| SyncEventSessionNextReasoningEnded
|
||||
| SyncEventSessionNextToolInputStarted
|
||||
| SyncEventSessionNextToolInputDelta
|
||||
| SyncEventSessionNextToolInputEnded
|
||||
| SyncEventSessionNextToolCalled
|
||||
| SyncEventSessionNextToolProgress
|
||||
| SyncEventSessionNextToolSuccess
|
||||
| SyncEventSessionNextToolFailed
|
||||
| SyncEventSessionNextRetried
|
||||
| SyncEventSessionNextCompactionStarted
|
||||
| SyncEventSessionNextCompactionDelta
|
||||
| SyncEventSessionNextCompactionEnded
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2403,12 +2374,20 @@ export type SyncEventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSync = {
|
||||
name: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
data: unknown
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
@@ -2417,6 +2396,7 @@ export type EventGlobalDisposed = {
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
@@ -2425,6 +2405,7 @@ export type EventServerInstanceDisposed = {
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventFileEdited = {
|
||||
@@ -2433,6 +2414,7 @@ export type EventFileEdited = {
|
||||
properties: {
|
||||
file: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventFileWatcherUpdated = {
|
||||
@@ -2442,6 +2424,7 @@ export type EventFileWatcherUpdated = {
|
||||
file: string
|
||||
event: "add" | "change" | "unlink"
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventLspClientDiagnostics = {
|
||||
@@ -2451,6 +2434,7 @@ export type EventLspClientDiagnostics = {
|
||||
serverID: string
|
||||
path: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventLspUpdated = {
|
||||
@@ -2459,6 +2443,7 @@ export type EventLspUpdated = {
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMessagePartDelta = {
|
||||
@@ -2471,12 +2456,14 @@ export type EventMessagePartDelta = {
|
||||
field: string
|
||||
delta: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPermissionAsked = {
|
||||
id: string
|
||||
type: "permission.asked"
|
||||
properties: PermissionRequest
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPermissionReplied = {
|
||||
@@ -2487,6 +2474,7 @@ export type EventPermissionReplied = {
|
||||
requestID: string
|
||||
reply: "once" | "always" | "reject"
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionDiff = {
|
||||
@@ -2496,6 +2484,7 @@ export type EventSessionDiff = {
|
||||
sessionID: string
|
||||
diff: Array<SnapshotFileDiff>
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionError = {
|
||||
@@ -2512,24 +2501,28 @@ export type EventSessionError = {
|
||||
| ContextOverflowError
|
||||
| ApiError
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventQuestionAsked = {
|
||||
id: string
|
||||
type: "question.asked"
|
||||
properties: QuestionRequest
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventQuestionReplied = {
|
||||
id: string
|
||||
type: "question.replied"
|
||||
properties: QuestionReplied
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventQuestionRejected = {
|
||||
id: string
|
||||
type: "question.rejected"
|
||||
properties: QuestionRejected
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTodoUpdated = {
|
||||
@@ -2539,6 +2532,7 @@ export type EventTodoUpdated = {
|
||||
sessionID: string
|
||||
todos: Array<Todo>
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionStatus = {
|
||||
@@ -2548,6 +2542,7 @@ export type EventSessionStatus = {
|
||||
sessionID: string
|
||||
status: SessionStatus
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionIdle = {
|
||||
@@ -2556,6 +2551,7 @@ export type EventSessionIdle = {
|
||||
properties: {
|
||||
sessionID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMcpToolsChanged = {
|
||||
@@ -2564,6 +2560,7 @@ export type EventMcpToolsChanged = {
|
||||
properties: {
|
||||
server: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMcpBrowserOpenFailed = {
|
||||
@@ -2573,6 +2570,7 @@ export type EventMcpBrowserOpenFailed = {
|
||||
mcpName: string
|
||||
url: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventCommandExecuted = {
|
||||
@@ -2584,12 +2582,14 @@ export type EventCommandExecuted = {
|
||||
arguments: string
|
||||
messageID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventProjectUpdated = {
|
||||
id: string
|
||||
type: "project.updated"
|
||||
properties: Project
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionCompacted = {
|
||||
@@ -2598,6 +2598,7 @@ export type EventSessionCompacted = {
|
||||
properties: {
|
||||
sessionID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventVcsBranchUpdated = {
|
||||
@@ -2606,6 +2607,7 @@ export type EventVcsBranchUpdated = {
|
||||
properties: {
|
||||
branch?: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
@@ -2614,6 +2616,7 @@ export type EventWorkspaceReady = {
|
||||
properties: {
|
||||
name: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
@@ -2622,6 +2625,7 @@ export type EventWorkspaceFailed = {
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventWorkspaceStatus = {
|
||||
@@ -2631,6 +2635,7 @@ export type EventWorkspaceStatus = {
|
||||
workspaceID: string
|
||||
status: "connected" | "connecting" | "disconnected" | "error"
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
@@ -2640,6 +2645,7 @@ export type EventWorktreeReady = {
|
||||
name: string
|
||||
branch?: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
@@ -2648,6 +2654,7 @@ export type EventWorktreeFailed = {
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPtyCreated = {
|
||||
@@ -2656,6 +2663,7 @@ export type EventPtyCreated = {
|
||||
properties: {
|
||||
info: Pty
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPtyUpdated = {
|
||||
@@ -2664,6 +2672,7 @@ export type EventPtyUpdated = {
|
||||
properties: {
|
||||
info: Pty
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPtyExited = {
|
||||
@@ -2673,6 +2682,7 @@ export type EventPtyExited = {
|
||||
id: string
|
||||
exitCode: number
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventPtyDeleted = {
|
||||
@@ -2681,6 +2691,7 @@ export type EventPtyDeleted = {
|
||||
properties: {
|
||||
id: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventInstallationUpdated = {
|
||||
@@ -2689,6 +2700,7 @@ export type EventInstallationUpdated = {
|
||||
properties: {
|
||||
version: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventInstallationUpdateAvailable = {
|
||||
@@ -2697,6 +2709,7 @@ export type EventInstallationUpdateAvailable = {
|
||||
properties: {
|
||||
version: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMessageUpdated = {
|
||||
@@ -2706,6 +2719,7 @@ export type EventMessageUpdated = {
|
||||
sessionID: string
|
||||
info: Message
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMessageRemoved = {
|
||||
@@ -2715,6 +2729,7 @@ export type EventMessageRemoved = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMessagePartUpdated = {
|
||||
@@ -2725,6 +2740,7 @@ export type EventMessagePartUpdated = {
|
||||
part: Part
|
||||
time: number
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventMessagePartRemoved = {
|
||||
@@ -2735,6 +2751,7 @@ export type EventMessagePartRemoved = {
|
||||
messageID: string
|
||||
partID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionCreated = {
|
||||
@@ -2744,6 +2761,7 @@ export type EventSessionCreated = {
|
||||
sessionID: string
|
||||
info: Session
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionUpdated = {
|
||||
@@ -2753,6 +2771,7 @@ export type EventSessionUpdated = {
|
||||
sessionID: string
|
||||
info: Session
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionDeleted = {
|
||||
@@ -2762,6 +2781,7 @@ export type EventSessionDeleted = {
|
||||
sessionID: string
|
||||
info: Session
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextAgentSwitched = {
|
||||
@@ -2772,6 +2792,7 @@ export type EventSessionNextAgentSwitched = {
|
||||
sessionID: string
|
||||
agent: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextModelSwitched = {
|
||||
@@ -2786,6 +2807,7 @@ export type EventSessionNextModelSwitched = {
|
||||
variant: string
|
||||
}
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type PromptSource = {
|
||||
@@ -2827,6 +2849,7 @@ export type EventSessionNextPrompted = {
|
||||
sessionID: string
|
||||
prompt: Prompt
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextSynthetic = {
|
||||
@@ -2837,6 +2860,7 @@ export type EventSessionNextSynthetic = {
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextShellStarted = {
|
||||
@@ -2848,6 +2872,7 @@ export type EventSessionNextShellStarted = {
|
||||
callID: string
|
||||
command: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextShellEnded = {
|
||||
@@ -2859,6 +2884,7 @@ export type EventSessionNextShellEnded = {
|
||||
callID: string
|
||||
output: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextStepStarted = {
|
||||
@@ -2875,6 +2901,7 @@ export type EventSessionNextStepStarted = {
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextStepEnded = {
|
||||
@@ -2896,6 +2923,7 @@ export type EventSessionNextStepEnded = {
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type SessionErrorUnknown = {
|
||||
@@ -2911,6 +2939,7 @@ export type EventSessionNextStepFailed = {
|
||||
sessionID: string
|
||||
error: SessionErrorUnknown
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextTextStarted = {
|
||||
@@ -2920,6 +2949,7 @@ export type EventSessionNextTextStarted = {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextTextDelta = {
|
||||
@@ -2930,6 +2960,7 @@ export type EventSessionNextTextDelta = {
|
||||
sessionID: string
|
||||
delta: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextTextEnded = {
|
||||
@@ -2940,6 +2971,7 @@ export type EventSessionNextTextEnded = {
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningStarted = {
|
||||
@@ -2950,6 +2982,7 @@ export type EventSessionNextReasoningStarted = {
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningDelta = {
|
||||
@@ -2961,6 +2994,7 @@ export type EventSessionNextReasoningDelta = {
|
||||
reasoningID: string
|
||||
delta: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningEnded = {
|
||||
@@ -2972,6 +3006,7 @@ export type EventSessionNextReasoningEnded = {
|
||||
reasoningID: string
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputStarted = {
|
||||
@@ -2983,6 +3018,7 @@ export type EventSessionNextToolInputStarted = {
|
||||
callID: string
|
||||
name: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputDelta = {
|
||||
@@ -2994,6 +3030,7 @@ export type EventSessionNextToolInputDelta = {
|
||||
callID: string
|
||||
delta: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputEnded = {
|
||||
@@ -3005,6 +3042,7 @@ export type EventSessionNextToolInputEnded = {
|
||||
callID: string
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolCalled = {
|
||||
@@ -3025,6 +3063,7 @@ export type EventSessionNextToolCalled = {
|
||||
}
|
||||
}
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type ToolTextContent = {
|
||||
@@ -3051,6 +3090,7 @@ export type EventSessionNextToolProgress = {
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolSuccess = {
|
||||
@@ -3071,6 +3111,7 @@ export type EventSessionNextToolSuccess = {
|
||||
}
|
||||
}
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextToolFailed = {
|
||||
@@ -3088,6 +3129,7 @@ export type EventSessionNextToolFailed = {
|
||||
}
|
||||
}
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type SessionNextRetryError = {
|
||||
@@ -3112,6 +3154,7 @@ export type EventSessionNextRetried = {
|
||||
attempt: number
|
||||
error: SessionNextRetryError
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionStarted = {
|
||||
@@ -3122,6 +3165,7 @@ export type EventSessionNextCompactionStarted = {
|
||||
sessionID: string
|
||||
reason: "auto" | "manual"
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionDelta = {
|
||||
@@ -3132,6 +3176,7 @@ export type EventSessionNextCompactionDelta = {
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionEnded = {
|
||||
@@ -3143,6 +3188,7 @@ export type EventSessionNextCompactionEnded = {
|
||||
text: string
|
||||
include?: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type ModelV2Info = {
|
||||
@@ -3249,6 +3295,7 @@ export type EventCatalogModelUpdated = {
|
||||
properties: {
|
||||
model: ModelV2Info
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
@@ -3553,6 +3600,41 @@ export type ProviderV2Info = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend1 = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute1 = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiToastShow1 = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
@@ -3562,6 +3644,19 @@ export type EventTuiToastShow1 = {
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect1 = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
sync?: EventSync
|
||||
}
|
||||
|
||||
export type ModelV2Info1 = {
|
||||
|
||||
@@ -68,8 +68,6 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/
|
||||
| Session processor effect tests do not require repository state | Removed git setup from all processor-effect temp server fixtures | 12.500s | 9.230s | keep | Two targeted reruns passed after the change: 9.61s, 9.23s. |
|
||||
| HTTP listen PTY ticket tests restart the same listener topology twice | Folded directory-scoped ticket regression into the broader unsafe-ticket test | 7.051s | 6.170s | keep | Two targeted reruns passed after the change: 6.76s, 6.17s; still covers mint failure and successful same-directory upgrade. |
|
||||
| File watcher readiness can write before async native subscriptions are active | Retried short readiness writes and accepted symlink-realpath HEAD events | failed | 4.62s | keep | Three sequential focused watcher runs passed: 4.62s, 4.57s, 4.64s; full suite no longer failed in `watcher.test.ts`. |
|
||||
| First provider config/env/filtering block can use Effect-aware instance fixtures | Migrated six `tmpdir` + `withTestInstance` cases to `it.instance` | 6.06s | 6.07s | keep | Neutral timing, but removes manual config file writes and instance plumbing; use as the pattern for later provider slices. |
|
||||
| Custom provider/model config cases can use Effect-aware instance fixtures | Migrated three more config-heavy provider cases to `it.instance` | 6.07s | 6.12s | keep | Neutral timing within noise, but continues removing manual config file writes on top of the first provider fixture PR. |
|
||||
|
||||
## Profiling Results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user