History

fix: delegate OpenCode session titles to provider (#3720)

Tristan Knight committed 3 months ago

Commit 32356581 parent 749baec2 files · +95 −4Browse files

Unified · Split

2 files
  1. apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +45 −1
  2. apps/server/src/provider/Layers/OpenCodeAdapter.ts +50 −3
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+45−1
@@ -54,6 +54,7 @@ const runtimeMock = { · 53 unmodified lines
5454 state: {
5555 startCalls: [] as string[],
5656 sessionCreateUrls: [] as string[],
57+ sessionCreateInputs: [] as Array<Record<string, unknown>>,
5758 authHeaders: [] as Array<string | null>,
5859 abortCalls: [] as string[],
5960 closeCalls: [] as string[],
@@ -67,6 +68,7 @@ const runtimeMock = { · 7 unmodified lines
6768 reset() {
6869 this.state.startCalls.length = 0;
6970 this.state.sessionCreateUrls.length = 0;
71+ this.state.sessionCreateInputs.length = 0;
7072 this.state.authHeaders.length = 0;
7173 this.state.abortCalls.length = 0;
7274 this.state.closeCalls.length = 0;
@@ -122,8 +124,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { · 49 unmodified lines
122124 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
123125 ({
124126 session: {
125− create: async () => {
127+ create: async (input: Record<string, unknown>) => {
126128 runtimeMock.state.sessionCreateUrls.push(baseUrl);
129+ runtimeMock.state.sessionCreateInputs.push(input);
127130 runtimeMock.state.authHeaders.push(
128131 serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,
129132 );
@@ -765,6 +768,47 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { · 635 unmodified lines
765768 }),
766769 );
767770
771+ it.effect("lets OpenCode own session title generation and emits title metadata updates", () =>
772+ Effect.gen(function* () {
773+ const adapter = yield* OpenCodeAdapter;
774+ const threadId = asThreadId("thread-opencode-title-sync");
775+ runtimeMock.state.subscribedEvents = [
776+ {
777+ type: "session.updated",
778+ properties: {
779+ info: {
780+ id: "http://127.0.0.1:9999/session",
781+ title: "Investigate OpenCode title sync",
782+ },
783+ },
784+ },
785+ ];
786+
787+ const eventsFiber = yield* adapter.streamEvents.pipe(
788+ Stream.filter((event) => event.threadId === threadId),
789+ Stream.take(3),
790+ Stream.runCollect,
791+ Effect.forkChild,
792+ );
793+
794+ yield* adapter.startSession({
795+ provider: ProviderDriverKind.make("opencode"),
796+ threadId,
797+ runtimeMode: "full-access",
798+ });
799+
800+ const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second")));
801+ NodeAssert.equal(runtimeMock.state.sessionCreateInputs.length, 1);
802+ NodeAssert.equal("title" in (runtimeMock.state.sessionCreateInputs[0] ?? {}), false);
803+
804+ const metadataUpdated = events.find((event) => event.type === "thread.metadata.updated");
805+ NodeAssert.ok(metadataUpdated);
806+ if (metadataUpdated.type === "thread.metadata.updated") {
807+ NodeAssert.equal(metadataUpdated.payload.name, "Investigate OpenCode title sync");
808+ }
809+ }),
810+ );
811+
768812 it.effect("writes provider-native observability records using the session thread id", () =>
769813 Effect.gen(function* () {
770814 const nativeEvents: Array<{
apps/server/src/provider/Layers/OpenCodeAdapter.ts+50−3
@@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent = · 64 unmodified lines
6565 ? TEvent
6666 : never;
6767
68+function trimText(value: string | undefined | null): string | undefined {
69+ const trimmed = value?.trim();
70+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
71+}
72+
73+function openCodeEventSessionId(event: OpenCodeSubscribedEvent): string | undefined {
74+ const properties = "properties" in event ? event.properties : undefined;
75+ if (!properties || typeof properties !== "object") {
76+ return undefined;
77+ }
78+
79+ const sessionID = (properties as { readonly sessionID?: unknown }).sessionID;
80+ const sessionIDFromProperties = typeof sessionID === "string" ? sessionID : undefined;
81+ if (sessionIDFromProperties) {
82+ return sessionIDFromProperties;
83+ }
84+
85+ const info = (properties as { readonly info?: { readonly id?: unknown } }).info;
86+ return info && typeof info.id === "string" ? info.id : undefined;
87+}
88+
89+function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | undefined {
90+ if (event.type !== "session.updated") {
91+ return undefined;
92+ }
93+
94+ return trimText(event.properties.info.title);
95+}
96+
6897 interface OpenCodeSessionContext {
6998 session: ProviderSession;
7099 readonly client: OpencodeClient;
@@ -643,8 +672,7 @@ export function makeOpenCodeAdapter( · 572 unmodified lines
643672 context: OpenCodeSessionContext,
644673 event: OpenCodeSubscribedEvent,
645674 ) {
646− const payloadSessionId =
647− "properties" in event ? (event.properties as { sessionID?: unknown }).sessionID : undefined;
675+ const payloadSessionId = openCodeEventSessionId(event);
648676 if (payloadSessionId !== context.openCodeSessionId) {
649677 return;
650678 }
@@ -663,6 +691,26 @@ export function makeOpenCodeAdapter( · 12 unmodified lines
663691 });
664692
665693 switch (event.type) {
694+ case "session.updated": {
695+ const title = openCodeEventSessionTitle(event);
696+ if (title) {
697+ yield* emit({
698+ ...(yield* buildEventBase({
699+ threadId: context.session.threadId,
700+ raw: event,
701+ })),
702+ type: "thread.metadata.updated",
703+ payload: {
704+ name: title,
705+ metadata: {
706+ sessionID: context.openCodeSessionId,
707+ },
708+ },
709+ });
710+ }
711+ break;
712+ }
713+
666714 case "message.updated": {
667715 context.messageRoleById.set(event.properties.info.id, event.properties.info.role);
668716 if (event.properties.info.role === "assistant") {
@@ -1069,7 +1117,6 @@ export function makeOpenCodeAdapter( · 400 unmodified lines
10691117 }
10701118 const openCodeSession = yield* runOpenCodeSdk("session.create", () =>
10711119 client.session.create({
1072− title: `T3 Code ${input.threadId}`,
10731120 permission: buildOpenCodePermissionRules(input.runtimeMode),
10741121 }),
10751122 );