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