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
54 state: {54 state: {
55 startCalls: [] as string[],55 startCalls: [] as string[],
56 sessionCreateUrls: [] as string[],56 sessionCreateUrls: [] as string[],
57 sessionCreateInputs: [] as Array<Record<string, unknown>>,
57 authHeaders: [] as Array<string | null>,58 authHeaders: [] as Array<string | null>,
58 abortCalls: [] as string[],59 abortCalls: [] as string[],
59 closeCalls: [] as string[],60 closeCalls: [] as string[],
@@ -67,6 +68,7 @@ const runtimeMock = { · 7 unmodified lines
67 reset() {68 reset() {
68 this.state.startCalls.length = 0;69 this.state.startCalls.length = 0;
69 this.state.sessionCreateUrls.length = 0;70 this.state.sessionCreateUrls.length = 0;
71 this.state.sessionCreateInputs.length = 0;
70 this.state.authHeaders.length = 0;72 this.state.authHeaders.length = 0;
71 this.state.abortCalls.length = 0;73 this.state.abortCalls.length = 0;
72 this.state.closeCalls.length = 0;74 this.state.closeCalls.length = 0;
@@ -122,8 +124,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { · 49 unmodified lines
122 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>124 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
123 ({125 ({
124 session: {126 session: {
125 create: async () => {127 create: async (input: Record<string, unknown>) => {
126 runtimeMock.state.sessionCreateUrls.push(baseUrl);128 runtimeMock.state.sessionCreateUrls.push(baseUrl);
129 runtimeMock.state.sessionCreateInputs.push(input);
127 runtimeMock.state.authHeaders.push(130 runtimeMock.state.authHeaders.push(
128 serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,131 serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,
129 );132 );
@@ -765,6 +768,47 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { · 635 unmodified lines
765 }),768 }),
766 );769 );
767 770
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
768 it.effect("writes provider-native observability records using the session thread id", () =>812 it.effect("writes provider-native observability records using the session thread id", () =>
769 Effect.gen(function* () {813 Effect.gen(function* () {
770 const nativeEvents: Array<{814 const nativeEvents: Array<{
apps/server/src/provider/Layers/OpenCodeAdapter.ts+50−3
@@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent = · 64 unmodified lines
65 ? TEvent65 ? TEvent
66 : never;66 : never;
67 67
68function trimText(value: string | undefined | null): string | undefined {
69 const trimmed = value?.trim();
70 return trimmed && trimmed.length > 0 ? trimmed : undefined;
71}
72
73function 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
89function 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
68interface OpenCodeSessionContext {97interface OpenCodeSessionContext {
69 session: ProviderSession;98 session: ProviderSession;
70 readonly client: OpencodeClient;99 readonly client: OpencodeClient;
@@ -643,8 +672,7 @@ export function makeOpenCodeAdapter( · 572 unmodified lines
643 context: OpenCodeSessionContext,672 context: OpenCodeSessionContext,
644 event: OpenCodeSubscribedEvent,673 event: OpenCodeSubscribedEvent,
645 ) {674 ) {
646 const payloadSessionId =675 const payloadSessionId = openCodeEventSessionId(event);
647 "properties" in event ? (event.properties as { sessionID?: unknown }).sessionID : undefined;
648 if (payloadSessionId !== context.openCodeSessionId) {676 if (payloadSessionId !== context.openCodeSessionId) {
649 return;677 return;
650 }678 }
@@ -663,6 +691,26 @@ export function makeOpenCodeAdapter( · 12 unmodified lines
663 });691 });
664 692
665 switch (event.type) {693 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
666 case "message.updated": {714 case "message.updated": {
667 context.messageRoleById.set(event.properties.info.id, event.properties.info.role);715 context.messageRoleById.set(event.properties.info.id, event.properties.info.role);
668 if (event.properties.info.role === "assistant") {716 if (event.properties.info.role === "assistant") {
@@ -1069,7 +1117,6 @@ export function makeOpenCodeAdapter( · 400 unmodified lines
1069 }1117 }
1070 const openCodeSession = yield* runOpenCodeSdk("session.create", () =>1118 const openCodeSession = yield* runOpenCodeSdk("session.create", () =>
1071 client.session.create({1119 client.session.create({
1072 title: `T3 Code ${input.threadId}`,
1073 permission: buildOpenCodePermissionRules(input.runtimeMode),1120 permission: buildOpenCodePermissionRules(input.runtimeMode),
1074 }),1121 }),
1075 );1122 );