Unified · Split
| @@ -54,6 +54,7 @@ const runtimeMock = { · 53 unmodified lines | ||
| 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 = { · 7 unmodified lines | ||
| 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 = { · 49 unmodified lines | ||
| 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) => { · 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 | 812 | it.effect("writes provider-native observability records using the session thread id", () => |
| 769 | 813 | Effect.gen(function* () { |
| 770 | 814 | const nativeEvents: Array<{ |
| @@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent = · 64 unmodified lines | ||
| 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( · 572 unmodified lines | ||
| 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( · 12 unmodified lines | ||
| 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( · 400 unmodified lines | ||
| 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 | ); |