1- import type { OrchestrationThreadDetailSnapshot , ThreadId } from "@t3tools/contracts" ;
1+ import type {
2+ OrchestrationClientCapabilities ,
3+ OrchestrationThreadDetailSnapshot ,
4+ ThreadId ,
5+ } from "@t3tools/contracts" ;
26import * as Cause from "effect/Cause" ;
37import * as Context from "effect/Context" ;
48import * as Effect from "effect/Effect" ;
@@ -31,12 +35,14 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(
3135) ( function * ( input : {
3236 readonly prepared : PreparedConnection ;
3337 readonly threadId : ThreadId ;
38+ readonly clientCapabilities ?: OrchestrationClientCapabilities ;
3439 readonly signer : Option . Option < ManagedRelayDpopSigner [ "Service" ] > ;
3540 readonly timeoutMs ?: number ;
3641} ) {
42+ const supportsAudioAttachments = input . clientCapabilities ?. audioAttachments === true ;
3743 const requestUrl = environmentEndpointUrl (
3844 input . prepared . httpBaseUrl ,
39- `/api/orchestration/threads/${ input . threadId } ? audioAttachments=true` ,
45+ `/api/orchestration/threads/${ input . threadId } ${ supportsAudioAttachments ? "? audioAttachments=true" : "" } ` ,
4046 ) ;
4147 const client = yield * makeEnvironmentHttpApiClient ( input . prepared . httpBaseUrl ) ;
4248 const headers = yield * buildEnvironmentAuthHeaders (
@@ -52,7 +58,7 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(
5258 input . prepared . httpAuthorization ,
5359 client . orchestration . threadSnapshot ( {
5460 params : { threadId : input . threadId } ,
55- query : { audioAttachments : "true" } ,
61+ query : supportsAudioAttachments ? { audioAttachments : "true" } : { } ,
5662 headers,
5763 } ) ,
5864 ) ,
@@ -70,6 +76,7 @@ export type FetchEnvironmentThreadSnapshotError = RemoteEnvironmentRequestError;
7076export class ThreadSnapshotLoader extends Context . Service <
7177 ThreadSnapshotLoader ,
7278 {
79+ readonly clientCapabilities : OrchestrationClientCapabilities ;
7380 readonly load : (
7481 prepared : PreparedConnection ,
7582 threadId : ThreadId ,
@@ -81,41 +88,53 @@ export const threadSnapshotLoaderLayer: Layer.Layer<
8188 ThreadSnapshotLoader ,
8289 never ,
8390 HttpClient . HttpClient
84- > = Layer . effect (
85- ThreadSnapshotLoader ,
86- Effect . gen ( function * ( ) {
87- const httpClient = yield * HttpClient . HttpClient ;
88- // Resolve the DPoP signer optionally: it is only needed for relay/DPoP
89- // connections, so the loader must not hard-require it (bearer/primary
90- // connections work without one).
91- const signer = yield * Effect . serviceOption ( ManagedRelayDpopSigner ) ;
92- return ThreadSnapshotLoader . of ( {
93- load : ( prepared : PreparedConnection , threadId : ThreadId ) =>
94- fetchEnvironmentThreadSnapshot ( { prepared, threadId, signer } ) . pipe (
95- Effect . map ( Option . some < OrchestrationThreadDetailSnapshot > ) ,
96- Effect . provideService ( HttpClient . HttpClient , httpClient ) ,
97- // A genuinely missing thread (404) is expected — the socket
98- // subscription is the source of truth for thread existence and will
99- // surface the deletion — so don't treat it as an error worth warning
100- // about; just defer to the socket path.
101- Effect . catchTags ( {
102- EnvironmentResourceNotFoundError : ( ) =>
103- Effect . logDebug (
104- "Thread snapshot not found over HTTP; deferring to the socket subscription." ,
91+ > = makeThreadSnapshotLoaderLayer ( ) ;
92+
93+ export function makeThreadSnapshotLoaderLayer (
94+ clientCapabilities : OrchestrationClientCapabilities = { } ,
95+ ) : Layer . Layer < ThreadSnapshotLoader , never , HttpClient . HttpClient > {
96+ return Layer . effect (
97+ ThreadSnapshotLoader ,
98+ Effect . gen ( function * ( ) {
99+ const httpClient = yield * HttpClient . HttpClient ;
100+ // Resolve the DPoP signer optionally: it is only needed for relay/DPoP
101+ // connections, so the loader must not hard-require it (bearer/primary
102+ // connections work without one).
103+ const signer = yield * Effect . serviceOption ( ManagedRelayDpopSigner ) ;
104+ return ThreadSnapshotLoader . of ( {
105+ clientCapabilities,
106+ load : ( prepared : PreparedConnection , threadId : ThreadId ) =>
107+ fetchEnvironmentThreadSnapshot ( {
108+ prepared,
109+ threadId,
110+ signer,
111+ clientCapabilities,
112+ } ) . pipe (
113+ Effect . map ( Option . some < OrchestrationThreadDetailSnapshot > ) ,
114+ Effect . provideService ( HttpClient . HttpClient , httpClient ) ,
115+ // A genuinely missing thread (404) is expected — the socket
116+ // subscription is the source of truth for thread existence and will
117+ // surface the deletion — so don't treat it as an error worth warning
118+ // about; just defer to the socket path.
119+ Effect . catchTags ( {
120+ EnvironmentResourceNotFoundError : ( ) =>
121+ Effect . logDebug (
122+ "Thread snapshot not found over HTTP; deferring to the socket subscription." ,
123+ ) . pipe (
124+ Effect . annotateLogs ( { threadId } ) ,
125+ Effect . as ( Option . none < OrchestrationThreadDetailSnapshot > ( ) ) ,
126+ ) ,
127+ } ) ,
128+ Effect . catchCause ( ( cause ) =>
129+ Effect . logWarning (
130+ "Could not load the thread snapshot over HTTP; using the socket snapshot instead." ,
105131 ) . pipe (
106- Effect . annotateLogs ( { threadId } ) ,
132+ Effect . annotateLogs ( { threadId, cause : Cause . pretty ( cause ) } ) ,
107133 Effect . as ( Option . none < OrchestrationThreadDetailSnapshot > ( ) ) ,
108134 ) ,
109- } ) ,
110- Effect . catchCause ( ( cause ) =>
111- Effect . logWarning (
112- "Could not load the thread snapshot over HTTP; using the socket snapshot instead." ,
113- ) . pipe (
114- Effect . annotateLogs ( { threadId, cause : Cause . pretty ( cause ) } ) ,
115- Effect . as ( Option . none < OrchestrationThreadDetailSnapshot > ( ) ) ,
116135 ) ,
117136 ) ,
118- ) ,
119- } ) ;
120- } ) ,
121- ) ;
137+ } ) ;
138+ } ) ,
139+ ) ;
140+ }
0 commit comments