clay-server 2.34.0-beta.3 → 2.34.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ var abortController = null;
31
31
  var pendingPermissions = {}; // requestId -> resolve
32
32
  var pendingAskUser = {}; // toolUseId -> resolve
33
33
  var pendingElicitations = {}; // requestId -> resolve
34
+ var pendingMcpToolCalls = {}; // requestId -> { resolve, reject }
34
35
  var conn = null;
35
36
  var buffer = "";
36
37
 
@@ -81,6 +82,90 @@ function getSDK() {
81
82
  return sdkModule;
82
83
  }
83
84
 
85
+ function buildZodShape(z, inputSchema) {
86
+ if (!inputSchema || !inputSchema.properties) return {};
87
+ var shape = {};
88
+ var props = inputSchema.properties;
89
+ var required = inputSchema.required || [];
90
+ var keys = Object.keys(props);
91
+
92
+ for (var i = 0; i < keys.length; i++) {
93
+ var key = keys[i];
94
+ var prop = props[key];
95
+ var field;
96
+
97
+ if (prop.type === "number" || prop.type === "integer") {
98
+ field = z.number();
99
+ } else if (prop.type === "boolean") {
100
+ field = z.boolean();
101
+ } else if (prop.type === "array") {
102
+ field = z.array(z.any());
103
+ } else if (prop.type === "object") {
104
+ field = z.record(z.any());
105
+ } else if (prop.enum) {
106
+ field = z.enum(prop.enum);
107
+ } else {
108
+ field = z.string();
109
+ }
110
+
111
+ if (prop.description) field = field.describe(prop.description);
112
+ if (required.indexOf(key) === -1) field = field.optional();
113
+ shape[key] = field;
114
+ }
115
+
116
+ return shape;
117
+ }
118
+
119
+ function createWorkerMcpToolHandler(serverName, toolName) {
120
+ return function(args) {
121
+ var requestId = crypto.randomUUID();
122
+ sendToDaemon({
123
+ type: "mcp_tool_call",
124
+ requestId: requestId,
125
+ serverName: serverName,
126
+ toolName: toolName,
127
+ args: args || {},
128
+ });
129
+ return new Promise(function(resolve, reject) {
130
+ pendingMcpToolCalls[requestId] = { resolve: resolve, reject: reject };
131
+ });
132
+ };
133
+ }
134
+
135
+ function buildMcpServersFromDescriptors(descriptors, sdk) {
136
+ if (!descriptors || !descriptors.length) return null;
137
+ var z;
138
+ try { z = require("zod").z; } catch (e) {
139
+ try { z = require("zod"); } catch (e2) { return null; }
140
+ }
141
+
142
+ var servers = {};
143
+ for (var i = 0; i < descriptors.length; i++) {
144
+ var descriptor = descriptors[i];
145
+ if (!descriptor || !descriptor.serverName || !descriptor.tools || !descriptor.tools.length) continue;
146
+ var tools = [];
147
+ for (var j = 0; j < descriptor.tools.length; j++) {
148
+ var toolDescriptor = descriptor.tools[j];
149
+ if (!toolDescriptor || !toolDescriptor.name) continue;
150
+ tools.push(sdk.tool(
151
+ toolDescriptor.name,
152
+ toolDescriptor.description || toolDescriptor.name,
153
+ buildZodShape(z, toolDescriptor.inputSchema),
154
+ createWorkerMcpToolHandler(descriptor.serverName, toolDescriptor.name)
155
+ ));
156
+ }
157
+ if (tools.length > 0) {
158
+ servers[descriptor.serverName] = sdk.createSdkMcpServer({
159
+ name: descriptor.serverName,
160
+ version: "1.0.0",
161
+ tools: tools,
162
+ });
163
+ }
164
+ }
165
+
166
+ return Object.keys(servers).length > 0 ? servers : null;
167
+ }
168
+
84
169
  // --- IPC helpers ---
85
170
  function sendToDaemon(msg) {
86
171
  if (!conn || conn.destroyed) return;
@@ -127,6 +212,9 @@ function handleMessage(msg) {
127
212
  case "elicitation_response":
128
213
  handleElicitationResponse(msg);
129
214
  break;
215
+ case "mcp_tool_result":
216
+ handleMcpToolResult(msg);
217
+ break;
130
218
  case "warmup":
131
219
  handleWarmup(msg);
132
220
  break;
@@ -208,6 +296,17 @@ function handleElicitationResponse(msg) {
208
296
  }
209
297
  }
210
298
 
299
+ function handleMcpToolResult(msg) {
300
+ var pending = pendingMcpToolCalls[msg.requestId];
301
+ if (!pending) return;
302
+ delete pendingMcpToolCalls[msg.requestId];
303
+ if (msg.error) {
304
+ pending.reject(new Error(msg.error));
305
+ return;
306
+ }
307
+ pending.resolve(msg.result);
308
+ }
309
+
211
310
  // --- Query handling ---
212
311
  async function handleQueryStart(msg) {
213
312
  var t0 = msg._perfT0 || Date.now();
@@ -238,6 +337,15 @@ async function handleQueryStart(msg) {
238
337
  options.abortController = abortController;
239
338
  options.debug = true;
240
339
  options.debugFile = "/tmp/clay-cli-debug-" + process.pid + ".log";
340
+ if (options.mcpServerDescriptors && options.mcpServerDescriptors.length) {
341
+ try {
342
+ var mcpServers = buildMcpServersFromDescriptors(options.mcpServerDescriptors, sdk);
343
+ if (mcpServers) options.mcpServers = mcpServers;
344
+ } catch (e) {
345
+ console.error("[sdk-worker] Failed to build MCP servers:", e.message || e);
346
+ }
347
+ delete options.mcpServerDescriptors;
348
+ }
241
349
  // Override CLI subprocess spawn to inject NODE_OPTIONS for IPv4-first DNS.
242
350
  // The SDK constructs its own env for the CLI process, so worker env vars
243
351
  // like NODE_OPTIONS are not inherited. We intercept the spawn to fix this.
@@ -648,7 +648,7 @@ function cleanupWorker(worker) {
648
648
  // in-process QueryHandle. This allows processQueryStream to iterate a worker
649
649
  // query identically to an in-process query.
650
650
 
651
- function createWorkerQueryHandle(worker, canUseTool, onElicitation) {
651
+ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool) {
652
652
  // Async iterable state
653
653
  var iterQueue = [];
654
654
  var iterWaiting = null;
@@ -741,6 +741,20 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation) {
741
741
  }
742
742
  break;
743
743
 
744
+ case "mcp_tool_call":
745
+ if (callMcpTool) {
746
+ callMcpTool(msg.serverName, msg.toolName, msg.args || {}).then(function(result) {
747
+ worker.send({ type: "mcp_tool_result", requestId: msg.requestId, result: result });
748
+ }).catch(function(e) {
749
+ worker.send({
750
+ type: "mcp_tool_result",
751
+ requestId: msg.requestId,
752
+ error: (e && e.message) ? e.message : String(e),
753
+ });
754
+ });
755
+ }
756
+ break;
757
+
744
758
  case "context_usage":
745
759
  case "model_changed":
746
760
  case "effort_changed":
@@ -1231,7 +1245,7 @@ function createClaudeAdapter(opts) {
1231
1245
  }
1232
1246
 
1233
1247
  // Create the worker query handle (sets up message handler on worker)
1234
- var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation);
1248
+ var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation, queryOpts.callMcpTool);
1235
1249
 
1236
1250
  // Wait for worker to be ready before sending query_start
1237
1251
  if (!reusingWorker) {
@@ -1254,7 +1268,7 @@ function createClaudeAdapter(opts) {
1254
1268
  if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
1255
1269
  if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
1256
1270
 
1257
- if (queryOpts.toolServers) queryOptions.mcpServers = queryOpts.toolServers;
1271
+ if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
1258
1272
  if (queryOpts.model) queryOptions.model = queryOpts.model;
1259
1273
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
1260
1274
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
@@ -89,6 +89,56 @@ function generateUuid() {
89
89
  return "codex-" + ts + "-" + cnt + "-" + rnd;
90
90
  }
91
91
 
92
+ function waitMs(ms) {
93
+ return new Promise(function(resolve) {
94
+ setTimeout(resolve, ms);
95
+ });
96
+ }
97
+
98
+ function waitForProcessExit(proc, timeoutMs) {
99
+ return new Promise(function(resolve) {
100
+ if (!proc) {
101
+ resolve(true);
102
+ return;
103
+ }
104
+
105
+ if (proc.exitCode !== null || proc.signalCode !== null) {
106
+ resolve(true);
107
+ return;
108
+ }
109
+
110
+ var done = false;
111
+ var timer = null;
112
+
113
+ function cleanup() {
114
+ if (done) return;
115
+ done = true;
116
+ if (timer) clearTimeout(timer);
117
+ proc.removeListener("exit", onDone);
118
+ proc.removeListener("close", onDone);
119
+ }
120
+
121
+ function onDone() {
122
+ cleanup();
123
+ resolve(true);
124
+ }
125
+
126
+ proc.once("exit", onDone);
127
+ proc.once("close", onDone);
128
+
129
+ timer = setTimeout(function() {
130
+ cleanup();
131
+ resolve(false);
132
+ }, timeoutMs || 5000);
133
+ });
134
+ }
135
+
136
+ function createShutdownError() {
137
+ var err = new Error("Codex adapter is shutting down, retry shortly");
138
+ err.code = "CODEX_ADAPTER_SHUTTING_DOWN";
139
+ return err;
140
+ }
141
+
92
142
  function normalizePlanStatus(status) {
93
143
  if (status === "inProgress") return "in_progress";
94
144
  if (status === "completed") return "completed";
@@ -310,16 +360,33 @@ function flattenEvent(notification, state) {
310
360
  state.thinkingBlocks[item.id] = "blk_" + state.blockCounter;
311
361
  events.push({ yokeType: "thinking_start", blockId: "blk_" + state.blockCounter });
312
362
  }
313
- if (item.text) {
363
+ // Codex reasoning items may expose plain text via `text`, a short
364
+ // `summary`, or nested `content` parts. Prefer whichever is present;
365
+ // many turns arrive with only encrypted reasoning and no readable
366
+ // text at all, in which case the UI will hide the expand affordance.
367
+ var reasoningText = "";
368
+ if (typeof item.text === "string" && item.text.length > 0) {
369
+ reasoningText = item.text;
370
+ } else if (typeof item.summary === "string" && item.summary.length > 0) {
371
+ reasoningText = item.summary;
372
+ } else if (Array.isArray(item.content)) {
373
+ var parts = [];
374
+ for (var rpi = 0; rpi < item.content.length; rpi++) {
375
+ var rp = item.content[rpi];
376
+ if (rp && typeof rp.text === "string") parts.push(rp.text);
377
+ }
378
+ reasoningText = parts.join("\n");
379
+ }
380
+ if (reasoningText) {
314
381
  var thinkBlockId = state.thinkingBlocks[item.id];
315
382
  var prevThinkLen = state.thinkingLengths[item.id] || 0;
316
- if (item.text.length > prevThinkLen) {
383
+ if (reasoningText.length > prevThinkLen) {
317
384
  events.push({
318
385
  yokeType: "thinking_delta",
319
386
  blockId: thinkBlockId,
320
- text: item.text.substring(prevThinkLen),
387
+ text: reasoningText.substring(prevThinkLen),
321
388
  });
322
- state.thinkingLengths[item.id] = item.text.length;
389
+ state.thinkingLengths[item.id] = reasoningText.length;
323
390
  }
324
391
  }
325
392
  if (evtPhase === "completed") {
@@ -503,6 +570,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
503
570
  var systemPrompt = queryOpts.systemPrompt || "";
504
571
  var canUseTool = queryOpts.canUseTool || null;
505
572
  var onElicitation = queryOpts.onElicitation || null;
573
+ var onFinished = queryOpts.onFinished || null;
506
574
 
507
575
  // Check if the query was cancelled (either via handle.abort() or direct signal abort)
508
576
  function isCancelled() {
@@ -533,6 +601,19 @@ function createCodexQueryHandle(appServer, queryOpts) {
533
601
  var eventBuffer = [];
534
602
  var eventWaiting = null;
535
603
  var iteratorDone = false;
604
+ var finishedNotified = false;
605
+
606
+ function notifyFinished() {
607
+ if (finishedNotified) return;
608
+ finishedNotified = true;
609
+ if (typeof onFinished === "function") {
610
+ try {
611
+ onFinished();
612
+ } catch (e) {
613
+ console.error("[yoke/codex] onFinished error:", e.message || e);
614
+ }
615
+ }
616
+ }
536
617
 
537
618
  function pushEvent(evt) {
538
619
  if (iteratorDone) return;
@@ -552,6 +633,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
552
633
  eventWaiting = null;
553
634
  resolve({ value: undefined, done: true });
554
635
  }
636
+ notifyFinished();
555
637
  }
556
638
 
557
639
  // Message queue for multi-turn
@@ -936,48 +1018,182 @@ function createCodexQueryHandle(appServer, queryOpts) {
936
1018
 
937
1019
  function createCodexAdapter(opts) {
938
1020
  var _cwd = (opts && opts.cwd) || process.cwd();
1021
+ var _slug = (opts && opts.slug) || "";
1022
+ var _defaultInitOpts = Object.assign({}, opts || {});
939
1023
  var _cachedModels = [];
940
1024
  var _appServer = null;
941
1025
  var _initPromise = null;
942
- var _initOpts = null; // stored for query-time access
1026
+ var _shutdownPromise = null;
1027
+ var _refCount = 0;
1028
+ var _lastActiveAt = Date.now();
1029
+ var _shuttingDown = false;
1030
+ var _activeQueries = [];
1031
+
1032
+ function updateLastActiveAt() {
1033
+ _lastActiveAt = Date.now();
1034
+ }
1035
+
1036
+ function registerActiveQuery(entry) {
1037
+ _activeQueries.push(entry);
1038
+ }
1039
+
1040
+ function removeActiveQuery(entry) {
1041
+ var next = [];
1042
+ for (var i = 0; i < _activeQueries.length; i++) {
1043
+ if (_activeQueries[i] !== entry) next.push(_activeQueries[i]);
1044
+ }
1045
+ _activeQueries = next;
1046
+ }
1047
+
1048
+ function decrementRefCount() {
1049
+ if (_refCount > 0) {
1050
+ _refCount--;
1051
+ } else {
1052
+ console.error("[yoke/codex] refCount negative, bug!");
1053
+ _refCount = 0;
1054
+ }
1055
+ updateLastActiveAt();
1056
+ }
1057
+
1058
+ function buildReadyResponse(skillNames) {
1059
+ return {
1060
+ models: _cachedModels,
1061
+ defaultModel: "gpt-5.4",
1062
+ skills: skillNames || [],
1063
+ slashCommands: skillNames || [],
1064
+ fastModeState: null,
1065
+ capabilities: {
1066
+ thinking: true,
1067
+ betas: false,
1068
+ rewind: false,
1069
+ sessionResume: true,
1070
+ promptSuggestions: true,
1071
+ elicitation: true,
1072
+ fileCheckpointing: false,
1073
+ contextCompacting: false,
1074
+ toolPolicy: ["ask", "allow-all"],
1075
+ },
1076
+ };
1077
+ }
1078
+
1079
+ function clearRuntimeState() {
1080
+ _appServer = null;
1081
+ _initPromise = null;
1082
+ _cachedModels = [];
1083
+ _refCount = 0;
1084
+ _activeQueries = [];
1085
+ updateLastActiveAt();
1086
+ }
1087
+
1088
+ function waitForRefCount(targetCount, timeoutMs) {
1089
+ var deadline = Date.now() + (timeoutMs || 5000);
1090
+ return new Promise(function(resolve) {
1091
+ function tick() {
1092
+ if (_refCount <= targetCount) {
1093
+ resolve(true);
1094
+ return;
1095
+ }
1096
+ if (Date.now() >= deadline) {
1097
+ resolve(false);
1098
+ return;
1099
+ }
1100
+ setTimeout(tick, 50);
1101
+ }
1102
+ tick();
1103
+ });
1104
+ }
1105
+
1106
+ function stopAppServer(deadlineMs) {
1107
+ var proc = _appServer && _appServer.proc ? _appServer.proc : null;
1108
+ if (!_appServer) return Promise.resolve(true);
1109
+ try {
1110
+ _appServer.stop();
1111
+ } catch (e) {
1112
+ console.error("[yoke/codex] App-server stop error:", e.message || e);
1113
+ }
1114
+ if (!proc) return Promise.resolve(true);
1115
+ var remaining = (typeof deadlineMs === "number") ? Math.max(0, deadlineMs - Date.now()) : 5000;
1116
+ return waitForProcessExit(proc, remaining).then(function(exited) {
1117
+ if (!exited) {
1118
+ try {
1119
+ proc.kill("SIGKILL");
1120
+ } catch (e) {}
1121
+ }
1122
+ return exited;
1123
+ });
1124
+ }
1125
+
1126
+ function beginShutdown(force, idleMs) {
1127
+ if (_shutdownPromise) return _shutdownPromise;
1128
+ if (_shuttingDown) return null;
1129
+
1130
+ _shuttingDown = true;
1131
+
1132
+ _shutdownPromise = (async function() {
1133
+ var deadline = Date.now() + 5000;
1134
+ var shouldAbort = !!force;
1135
+
1136
+ if (_initPromise) {
1137
+ try {
1138
+ await Promise.race([
1139
+ _initPromise.catch(function() { return null; }),
1140
+ waitMs(Math.max(0, deadline - Date.now())),
1141
+ ]);
1142
+ } catch (e) {}
1143
+ }
1144
+
1145
+ if (shouldAbort && _activeQueries.length > 0) {
1146
+ var active = _activeQueries.slice();
1147
+ for (var i = 0; i < active.length; i++) {
1148
+ try {
1149
+ if (active[i] && active[i].abort) active[i].abort();
1150
+ } catch (e) {}
1151
+ }
1152
+ await waitForRefCount(0, Math.max(0, deadline - Date.now()));
1153
+ }
1154
+
1155
+ if (_appServer) {
1156
+ await stopAppServer(deadline);
1157
+ }
1158
+
1159
+ clearRuntimeState();
1160
+ _shuttingDown = false;
1161
+ _shutdownPromise = null;
1162
+ return true;
1163
+ })().catch(function(err) {
1164
+ clearRuntimeState();
1165
+ _shuttingDown = false;
1166
+ _shutdownPromise = null;
1167
+ throw err;
1168
+ });
1169
+
1170
+ return _shutdownPromise;
1171
+ }
943
1172
 
944
1173
  var adapter = {
945
1174
  vendor: "codex",
946
1175
 
947
1176
  init: function(initOpts) {
1177
+ if (_shuttingDown) {
1178
+ return Promise.reject(createShutdownError());
1179
+ }
1180
+
1181
+ var effectiveInitOpts = Object.assign({}, _defaultInitOpts, initOpts || {});
1182
+
948
1183
  // Already initialized - return cached result
949
1184
  if (_appServer && _appServer.started && _cachedModels.length > 0) {
950
- return Promise.resolve({
951
- models: _cachedModels,
952
- defaultModel: "gpt-5.4",
953
- skills: [],
954
- slashCommands: [],
955
- fastModeState: null,
956
- capabilities: {
957
- thinking: true,
958
- betas: false,
959
- rewind: false,
960
- sessionResume: true,
961
- promptSuggestions: true,
962
- elicitation: true,
963
- fileCheckpointing: false,
964
- contextCompacting: false,
965
- toolPolicy: ["ask", "allow-all"],
966
- },
967
- });
1185
+ return Promise.resolve(buildReadyResponse([]));
968
1186
  }
969
1187
 
970
1188
  // Deduplicate concurrent init calls
971
1189
  if (_initPromise) return _initPromise;
972
1190
 
973
1191
  _initPromise = (async function() {
974
- _initOpts = initOpts;
975
-
976
1192
  var serverOpts = { cwd: _cwd };
977
1193
 
978
1194
  // Extract adapter options
979
- if (initOpts && initOpts.adapterOptions && initOpts.adapterOptions.CODEX) {
980
- var co = initOpts.adapterOptions.CODEX;
1195
+ if (effectiveInitOpts && effectiveInitOpts.adapterOptions && effectiveInitOpts.adapterOptions.CODEX) {
1196
+ var co = effectiveInitOpts.adapterOptions.CODEX;
981
1197
  if (co.apiKey) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_API_KEY: co.apiKey });
982
1198
  if (co.baseUrl) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_BASE_URL: co.baseUrl });
983
1199
  if (co.config) serverOpts.config = co.config;
@@ -1005,10 +1221,10 @@ function createCodexAdapter(opts) {
1005
1221
 
1006
1222
  // Track 2: Add clay-tools bridge server for in-app + remote MCP tools.
1007
1223
  var bridgePath = require("path").join(__dirname, "..", "mcp-bridge-server.js");
1008
- var clayPort = (initOpts && initOpts.clayPort) || process.env.CLAY_PORT || 2633;
1009
- var clayTls = (initOpts && initOpts.clayTls) || false;
1010
- var clayAuthToken = (initOpts && initOpts.clayAuthToken) || "";
1011
- var claySlug = (initOpts && initOpts.slug) || "";
1224
+ var clayPort = effectiveInitOpts.clayPort || process.env.CLAY_PORT || 2633;
1225
+ var clayTls = effectiveInitOpts.clayTls || false;
1226
+ var clayAuthToken = effectiveInitOpts.clayAuthToken || "";
1227
+ var claySlug = effectiveInitOpts.slug || _slug || "";
1012
1228
  try {
1013
1229
  if (require("fs").existsSync(bridgePath)) {
1014
1230
  var bridgeArgs = [bridgePath, "--port", String(clayPort), "--slug", claySlug];
@@ -1049,6 +1265,11 @@ function createCodexAdapter(opts) {
1049
1265
  });
1050
1266
  _appServer.notify("initialized", {});
1051
1267
 
1268
+ if (_shuttingDown) {
1269
+ await stopAppServer(Date.now() + 1000);
1270
+ throw createShutdownError();
1271
+ }
1272
+
1052
1273
  console.log("[codex] App-server initialized, models: gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2");
1053
1274
 
1054
1275
  _cachedModels = [
@@ -1097,26 +1318,15 @@ function createCodexAdapter(opts) {
1097
1318
  console.error("[codex] Failed to discover skills:", e.message);
1098
1319
  }
1099
1320
 
1321
+ if (_shuttingDown) {
1322
+ await stopAppServer(Date.now() + 1000);
1323
+ throw createShutdownError();
1324
+ }
1325
+
1100
1326
  _initPromise = null;
1327
+ updateLastActiveAt();
1101
1328
 
1102
- return {
1103
- models: _cachedModels,
1104
- defaultModel: "gpt-5.4",
1105
- skills: skillNames,
1106
- slashCommands: skillNames,
1107
- fastModeState: null,
1108
- capabilities: {
1109
- thinking: true,
1110
- betas: false,
1111
- rewind: false,
1112
- sessionResume: true,
1113
- promptSuggestions: true,
1114
- elicitation: true,
1115
- fileCheckpointing: false,
1116
- contextCompacting: false,
1117
- toolPolicy: ["ask", "allow-all"],
1118
- },
1119
- };
1329
+ return buildReadyResponse(skillNames);
1120
1330
  })();
1121
1331
 
1122
1332
  return _initPromise;
@@ -1134,12 +1344,31 @@ function createCodexAdapter(opts) {
1134
1344
  },
1135
1345
 
1136
1346
  createQuery: async function(queryOpts) {
1347
+ if (_shuttingDown) {
1348
+ throw createShutdownError();
1349
+ }
1350
+
1351
+ if (!_appServer || !_appServer.started) {
1352
+ await adapter.init(queryOpts || {});
1353
+ }
1354
+
1355
+ if (_shuttingDown) {
1356
+ throw createShutdownError();
1357
+ }
1358
+
1137
1359
  if (!_appServer || !_appServer.started) {
1138
1360
  throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
1139
1361
  }
1140
1362
 
1141
1363
  var model = queryOpts.model || "gpt-5.4";
1142
1364
  var ac = queryOpts.abortController || new AbortController();
1365
+ var activeEntry = {
1366
+ abort: function() {
1367
+ try {
1368
+ ac.abort();
1369
+ } catch (e) {}
1370
+ },
1371
+ };
1143
1372
 
1144
1373
  // Map YOKE options to Codex thread options
1145
1374
  var codexOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.CODEX) || {};
@@ -1176,7 +1405,33 @@ function createCodexAdapter(opts) {
1176
1405
 
1177
1406
  console.log("[yoke/codex] createQuery: model=" + model + " approval=" + handleOpts.approvalPolicy + " sandbox=" + handleOpts.sandboxMode);
1178
1407
 
1179
- var handle = createCodexQueryHandle(_appServer, handleOpts);
1408
+ _refCount++;
1409
+ registerActiveQuery(activeEntry);
1410
+
1411
+ var handle;
1412
+ try {
1413
+ handleOpts.onFinished = function() {
1414
+ removeActiveQuery(activeEntry);
1415
+ decrementRefCount();
1416
+ };
1417
+ handle = createCodexQueryHandle(_appServer, handleOpts);
1418
+ } catch (e) {
1419
+ removeActiveQuery(activeEntry);
1420
+ decrementRefCount();
1421
+ throw e;
1422
+ }
1423
+
1424
+ activeEntry.handle = handle;
1425
+ activeEntry.abort = function() {
1426
+ try {
1427
+ if (handle && typeof handle.abort === "function") {
1428
+ handle.abort();
1429
+ } else {
1430
+ ac.abort();
1431
+ }
1432
+ } catch (e) {}
1433
+ };
1434
+
1180
1435
  return handle;
1181
1436
  },
1182
1437
 
@@ -1243,10 +1498,19 @@ function createCodexAdapter(opts) {
1243
1498
 
1244
1499
  // Shutdown the app-server process
1245
1500
  shutdown: function() {
1246
- if (_appServer) {
1247
- _appServer.stop();
1248
- _appServer = null;
1249
- }
1501
+ return beginShutdown(true);
1502
+ },
1503
+
1504
+ shutdownIfIdle: function(idleMs) {
1505
+ if (_shuttingDown || _shutdownPromise) return Promise.resolve(false);
1506
+ if (_initPromise) return Promise.resolve(false);
1507
+ if (!_appServer) return Promise.resolve(false);
1508
+ if (_refCount > 0) return Promise.resolve(false);
1509
+ if (Date.now() - _lastActiveAt < (idleMs || 0)) return Promise.resolve(false);
1510
+ return beginShutdown(false).then(function() {
1511
+ console.log("[yoke/codex] Reclaimed idle adapter for project " + (_slug || _cwd));
1512
+ return true;
1513
+ });
1250
1514
  },
1251
1515
  };
1252
1516