@@ -77,7 +77,8 @@ public class CodexAppServerClient implements AutoCloseable {
7777 private final CodexAppServerConfig config ;
7878 private final ObjectMapper objectMapper =
7979 JsonMapper .builder ().disable (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES ).build ();
80- private final ThreadMappingCache threadBySession ;
80+ private final ThreadMappingStore threadBySession ;
81+ private final SessionExecutionCoordinator sessionCoordinator = new SessionExecutionCoordinator ();
8182 private final Object httpClientLock = new Object ();
8283 private final ExecutorService clientExecutor = Executors .newCachedThreadPool (r -> {
8384 Thread thread = new Thread (r , "codex-app-server-client" );
@@ -95,8 +96,15 @@ public class CodexAppServerClient implements AutoCloseable {
9596 * @throws NullPointerException if {@code config} is {@code null}.
9697 */
9798 public CodexAppServerClient (CodexAppServerConfig config ) {
99+ this (config , new ThreadMappingCache (
100+ Objects .requireNonNull (config , "config" ).getMaxSessionMappings ()));
101+ }
102+
103+ public CodexAppServerClient (
104+ CodexAppServerConfig config ,
105+ ThreadMappingStore threadMappingStore ) {
98106 this .config = Objects .requireNonNull (config , "config" );
99- this .threadBySession = new ThreadMappingCache ( config . getMaxSessionMappings () );
107+ this .threadBySession = Objects . requireNonNull ( threadMappingStore , "threadMappingStore" );
100108 }
101109
102110 /**
@@ -142,6 +150,19 @@ public CompletableFuture<AppServerTurnResult> runTurnAsync(AppServerTurnRequest
142150 if (Objects .isNull (request .getPrompt ()) || request .getPrompt ().trim ().isEmpty ()) {
143151 throw new IllegalArgumentException ("Codex prompt must not be blank" );
144152 }
153+ String sessionKey = request .normalizedSessionKey ();
154+ if (Objects .isNull (sessionKey )) {
155+ return startTurn (request );
156+ }
157+ return sessionCoordinator .submit (sessionKey , () -> startTurn (request ));
158+ }
159+
160+ private CompletableFuture <AppServerTurnResult > startTurn (AppServerTurnRequest request ) {
161+ if (closed ) {
162+ CompletableFuture <AppServerTurnResult > failed = new CompletableFuture <>();
163+ failed .completeExceptionally (new IllegalStateException ("Codex app-server client is closed" ));
164+ return failed ;
165+ }
145166 CodexAppServerTurn turn =
146167 new CodexAppServerTurn (request , config , objectMapper , threadBySession , httpClient ());
147168 return turn .start ();
@@ -187,7 +208,7 @@ public List<AppServerThread> listThreads(int limit, String cursor) {
187208 if (limit > 0 ) {
188209 params .put ("limit" , limit );
189210 }
190- JsonNode result = execRpcNode ("thread/list" , params );
211+ JsonNode result = execRpcNode (CodexAppServerProtocol . THREAD_LIST , params );
191212 List <AppServerThread > threads = new ArrayList <>();
192213 for (JsonNode node : result .path ("threads" )) {
193214 threads .add (parseThread (node ));
@@ -205,7 +226,7 @@ public List<AppServerThread> listThreads(int limit, String cursor) {
205226 public AppServerThread readThread (String threadId ) {
206227 Map <String , Object > params = new LinkedHashMap <>();
207228 params .put ("threadId" , Objects .requireNonNull (threadId , "threadId" ).trim ());
208- JsonNode result = execRpcNode ("thread/read" , params );
229+ JsonNode result = execRpcNode (CodexAppServerProtocol . THREAD_READ , params );
209230 return parseThread (result .path ("thread" ));
210231 }
211232
@@ -234,26 +255,26 @@ public String readThreadRaw(String threadId) {
234255 public AppServerThread forkThread (String threadId ) {
235256 Map <String , Object > params = new LinkedHashMap <>();
236257 params .put ("threadId" , Objects .requireNonNull (threadId , "threadId" ).trim ());
237- JsonNode result = execRpcNode ("thread/fork" , params );
258+ JsonNode result = execRpcNode (CodexAppServerProtocol . THREAD_FORK , params );
238259 return parseThread (result .path ("thread" ));
239260 }
240261
241262 /** Archives a thread via {@code thread/archive}. */
242263 public void archiveThread (String threadId ) {
243- simpleThreadCall ("thread/archive" , threadId );
264+ simpleThreadCall (CodexAppServerProtocol . THREAD_ARCHIVE , threadId );
244265 }
245266
246267 /** Unarchives a thread via {@code thread/unarchive}. */
247268 public void unarchiveThread (String threadId ) {
248- simpleThreadCall ("thread/unarchive" , threadId );
269+ simpleThreadCall (CodexAppServerProtocol . THREAD_UNARCHIVE , threadId );
249270 }
250271
251272 /**
252273 * Permanently deletes a thread and its spawned descendants via
253274 * {@code thread/delete}. Ephemeral roots cannot be deleted.
254275 */
255276 public void deleteThread (String threadId ) {
256- simpleThreadCall ("thread/delete" , threadId );
277+ simpleThreadCall (CodexAppServerProtocol . THREAD_DELETE , threadId );
257278 }
258279
259280 /**
@@ -269,7 +290,7 @@ public void interruptTurn(String threadId, String turnId) {
269290 Map <String , Object > params = new LinkedHashMap <>();
270291 params .put ("threadId" , Objects .requireNonNull (threadId , "threadId" ).trim ());
271292 params .put ("turnId" , Objects .requireNonNull (turnId , "turnId" ).trim ());
272- execRpcNode ("turn/interrupt" , params );
293+ execRpcNode (CodexAppServerProtocol . TURN_INTERRUPT , params );
273294 }
274295
275296 /**
@@ -290,7 +311,7 @@ public void steerTurn(String threadId, String expectedTurnId, String prompt) {
290311 params .put ("threadId" , Objects .requireNonNull (threadId , "threadId" ).trim ());
291312 params .put ("expectedTurnId" , Objects .requireNonNull (expectedTurnId , "expectedTurnId" ).trim ());
292313 params .put ("input" , List .of (input ));
293- execRpcNode ("turn/steer" , params );
314+ execRpcNode (CodexAppServerProtocol . TURN_STEER , params );
294315 }
295316
296317 /**
0 commit comments