Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const runtimeMock = {
state: {
startCalls: [] as string[],
sessionCreateUrls: [] as string[],
sessionCreateInputs: [] as Array<Record<string, unknown>>,
authHeaders: [] as Array<string | null>,
abortCalls: [] as string[],
closeCalls: [] as string[],
Expand All @@ -67,6 +68,7 @@ const runtimeMock = {
reset() {
this.state.startCalls.length = 0;
this.state.sessionCreateUrls.length = 0;
this.state.sessionCreateInputs.length = 0;
this.state.authHeaders.length = 0;
this.state.abortCalls.length = 0;
this.state.closeCalls.length = 0;
Expand Down Expand Up @@ -122,8 +124,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
({
session: {
create: async () => {
create: async (input: Record<string, unknown>) => {
runtimeMock.state.sessionCreateUrls.push(baseUrl);
runtimeMock.state.sessionCreateInputs.push(input);
runtimeMock.state.authHeaders.push(
serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,
);
Expand Down Expand Up @@ -733,6 +736,47 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
}),
);

it.effect("lets OpenCode own session title generation and emits title metadata updates", () =>
Effect.gen(function* () {
const adapter = yield* OpenCodeAdapter;
const threadId = asThreadId("thread-opencode-title-sync");
runtimeMock.state.subscribedEvents = [
{
type: "session.updated",
properties: {
info: {
id: "http://127.0.0.1:9999/session",
title: "Investigate OpenCode title sync",
},
},
},
];

const eventsFiber = yield* adapter.streamEvents.pipe(
Stream.filter((event) => event.threadId === threadId),
Stream.take(3),
Stream.runCollect,
Effect.forkChild,
);

yield* adapter.startSession({
provider: ProviderDriverKind.make("opencode"),
threadId,
runtimeMode: "full-access",
});

const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second")));
NodeAssert.equal(runtimeMock.state.sessionCreateInputs.length, 1);
NodeAssert.equal("title" in (runtimeMock.state.sessionCreateInputs[0] ?? {}), false);

const metadataUpdated = events.find((event) => event.type === "thread.metadata.updated");
NodeAssert.ok(metadataUpdated);
if (metadataUpdated.type === "thread.metadata.updated") {
NodeAssert.equal(metadataUpdated.payload.name, "Investigate OpenCode title sync");
}
}),
);

it.effect("writes provider-native observability records using the session thread id", () =>
Effect.gen(function* () {
const nativeEvents: Array<{
Expand Down
53 changes: 50 additions & 3 deletions apps/server/src/provider/Layers/OpenCodeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent =
? TEvent
: never;

function trimText(value: string | undefined | null): string | undefined {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : undefined;
}

function openCodeEventSessionId(event: OpenCodeSubscribedEvent): string | undefined {
const properties = "properties" in event ? event.properties : undefined;
if (!properties || typeof properties !== "object") {
return undefined;
}

const sessionID = (properties as { readonly sessionID?: unknown }).sessionID;
const sessionIDFromProperties = typeof sessionID === "string" ? sessionID : undefined;
if (sessionIDFromProperties) {
return sessionIDFromProperties;
}

const info = (properties as { readonly info?: { readonly id?: unknown } }).info;
return info && typeof info.id === "string" ? info.id : undefined;
}

function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | undefined {
if (event.type !== "session.updated") {
return undefined;
}

return trimText(event.properties.info.title);
}

interface OpenCodeSessionContext {
session: ProviderSession;
readonly client: OpencodeClient;
Expand Down Expand Up @@ -646,8 +675,7 @@ export function makeOpenCodeAdapter(
context: OpenCodeSessionContext,
event: OpenCodeSubscribedEvent,
) {
const payloadSessionId =
"properties" in event ? (event.properties as { sessionID?: unknown }).sessionID : undefined;
const payloadSessionId = openCodeEventSessionId(event);
if (payloadSessionId !== context.openCodeSessionId) {
return;
}
Expand All @@ -666,6 +694,26 @@ export function makeOpenCodeAdapter(
});

switch (event.type) {
case "session.updated": {
const title = openCodeEventSessionTitle(event);
if (title) {
yield* emit({
...(yield* buildEventBase({
threadId: context.session.threadId,
raw: event,
})),
type: "thread.metadata.updated",
payload: {
name: title,
metadata: {
sessionID: context.openCodeSessionId,
},
},
});
}
break;
}

case "message.updated": {
context.messageRoleById.set(event.properties.info.id, event.properties.info.role);
if (event.properties.info.role === "assistant") {
Expand Down Expand Up @@ -1072,7 +1120,6 @@ export function makeOpenCodeAdapter(
}
const openCodeSession = yield* runOpenCodeSdk("session.create", () =>
client.session.create({
title: `T3 Code ${input.threadId}`,
permission: buildOpenCodePermissionRules(input.runtimeMode),
}),
);
Expand Down
Loading