| author | |
| committer | |
| log | 3235658c080bc12fcd1ffaa275aced98d225f2f6 |
| tree | 79c1727ebfda14a9d6fa7afaf4936c0b53f0233d |
| parent | 749baec353d5675f1acac81f22aa15da573e71c7 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 95 insertions(+), 4 deletions(-)
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+45-1| ... | @@ -54,6 +54,7 @@ const runtimeMock = { | ... | @@ -54,6 +54,7 @@ const runtimeMock = { |
| 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 = { | ... | @@ -67,6 +68,7 @@ const runtimeMock = { |
| 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 = { | ... | @@ -122,8 +124,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { |
| 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) => { | ... | @@ -765,6 +768,47 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 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 = | ... | @@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent = |
| 65 | ? TEvent | 65 | ? TEvent |
| 66 | : never; | 66 | : never; |
| 67 | 67 | ||
| 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 | |||
| 68 | interface OpenCodeSessionContext { | 97 | interface OpenCodeSessionContext { |
| 69 | session: ProviderSession; | 98 | session: ProviderSession; |
| 70 | readonly client: OpencodeClient; | 99 | readonly client: OpencodeClient; |
| ... | @@ -643,8 +672,7 @@ export function makeOpenCodeAdapter( | ... | @@ -643,8 +672,7 @@ export function makeOpenCodeAdapter( |
| 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( | ... | @@ -663,6 +691,26 @@ export function makeOpenCodeAdapter( |
| 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( | ... | @@ -1069,7 +1117,6 @@ export function makeOpenCodeAdapter( |
| 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 | ); |