clay-server 2.33.1 → 2.34.0-beta.10

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.
Files changed (47) hide show
  1. package/lib/ask-user-mcp-server.js +120 -0
  2. package/lib/config.js +9 -13
  3. package/lib/daemon.js +116 -55
  4. package/lib/mate-datastore.js +359 -0
  5. package/lib/mates.js +2 -2
  6. package/lib/os-users.js +70 -37
  7. package/lib/project-connection.js +16 -9
  8. package/lib/project-http.js +3 -4
  9. package/lib/project-image.js +3 -2
  10. package/lib/project-mate-datastore.js +232 -0
  11. package/lib/project-sessions.js +110 -7
  12. package/lib/project-user-message.js +4 -3
  13. package/lib/project.js +126 -10
  14. package/lib/public/app.js +2 -0
  15. package/lib/public/css/mates.css +228 -11
  16. package/lib/public/css/messages.css +23 -0
  17. package/lib/public/css/mobile-nav.css +0 -14
  18. package/lib/public/css/notifications-center.css +80 -0
  19. package/lib/public/css/sidebar.css +326 -101
  20. package/lib/public/index.html +24 -29
  21. package/lib/public/modules/app-dm.js +0 -2
  22. package/lib/public/modules/app-messages.js +23 -0
  23. package/lib/public/modules/app-rendering.js +0 -2
  24. package/lib/public/modules/diff.js +21 -7
  25. package/lib/public/modules/mate-datastore-ui.js +280 -0
  26. package/lib/public/modules/mate-sidebar.js +3 -9
  27. package/lib/public/modules/mate-wizard.js +15 -15
  28. package/lib/public/modules/sidebar-mobile.js +10 -20
  29. package/lib/public/modules/sidebar-sessions.js +490 -113
  30. package/lib/public/modules/sidebar.js +8 -6
  31. package/lib/public/modules/tools.js +115 -18
  32. package/lib/public/sw.js +1 -1
  33. package/lib/sdk-bridge.js +56 -41
  34. package/lib/sdk-message-processor.js +21 -4
  35. package/lib/server.js +28 -72
  36. package/lib/sessions.js +157 -20
  37. package/lib/updater.js +2 -2
  38. package/lib/users.js +2 -2
  39. package/lib/ws-schema.js +16 -0
  40. package/lib/yoke/adapters/claude-worker.js +114 -2
  41. package/lib/yoke/adapters/claude.js +56 -5
  42. package/lib/yoke/adapters/codex.js +350 -58
  43. package/lib/yoke/index.js +93 -48
  44. package/lib/yoke/instructions.js +0 -1
  45. package/lib/yoke/mcp-bridge-server.js +14 -6
  46. package/package.json +1 -2
  47. package/lib/yoke/adapters/gemini.js +0 -709
@@ -600,7 +600,7 @@ function spawnWorker(linuxUser, workerScriptPath, cwd) {
600
600
  worker.send = function(msg) {
601
601
  if (!worker.connection || worker.connection.destroyed) return;
602
602
  try {
603
- worker.connection.write(JSON.stringify(msg) + "\n");
603
+ worker.connection.write(JSON.stringify(serializeWorkerValue(msg)) + "\n");
604
604
  } catch (e) {
605
605
  console.error("[yoke/claude] Failed to send to worker:", e.message);
606
606
  }
@@ -643,12 +643,45 @@ function cleanupWorker(worker) {
643
643
  worker.ready = false;
644
644
  }
645
645
 
646
+ function serializeWorkerValue(value, seen) {
647
+ if (value == null) return value;
648
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
649
+ if (typeof value === "bigint") return String(value);
650
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "undefined") return undefined;
651
+ if (value instanceof Date) return value.toISOString();
652
+ if (Buffer.isBuffer(value)) return value.toString("base64");
653
+
654
+ if (!seen) seen = new WeakSet();
655
+ if (typeof value === "object") {
656
+ if (seen.has(value)) return undefined;
657
+ seen.add(value);
658
+ }
659
+
660
+ if (Array.isArray(value)) {
661
+ var arr = [];
662
+ for (var i = 0; i < value.length; i++) {
663
+ var item = serializeWorkerValue(value[i], seen);
664
+ if (item !== undefined) arr.push(item);
665
+ }
666
+ return arr;
667
+ }
668
+
669
+ var out = {};
670
+ var keys = Object.keys(value);
671
+ for (var j = 0; j < keys.length; j++) {
672
+ var key = keys[j];
673
+ var child = serializeWorkerValue(value[key], seen);
674
+ if (child !== undefined) out[key] = child;
675
+ }
676
+ return out;
677
+ }
678
+
646
679
  // --- Worker QueryHandle ---
647
680
  // Wraps worker IPC into the same async iterable + control interface as the
648
681
  // in-process QueryHandle. This allows processQueryStream to iterate a worker
649
682
  // query identically to an in-process query.
650
683
 
651
- function createWorkerQueryHandle(worker, canUseTool, onElicitation) {
684
+ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool) {
652
685
  // Async iterable state
653
686
  var iterQueue = [];
654
687
  var iterWaiting = null;
@@ -741,6 +774,20 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation) {
741
774
  }
742
775
  break;
743
776
 
777
+ case "mcp_tool_call":
778
+ if (callMcpTool) {
779
+ callMcpTool(msg.serverName, msg.toolName, msg.args || {}).then(function(result) {
780
+ worker.send({ type: "mcp_tool_result", requestId: msg.requestId, result: result });
781
+ }).catch(function(e) {
782
+ worker.send({
783
+ type: "mcp_tool_result",
784
+ requestId: msg.requestId,
785
+ error: (e && e.message) ? e.message : String(e),
786
+ });
787
+ });
788
+ }
789
+ break;
790
+
744
791
  case "context_usage":
745
792
  case "model_changed":
746
793
  case "effort_changed":
@@ -943,6 +990,7 @@ function createClaudeAdapter(opts) {
943
990
  cwd: (initOpts && initOpts.cwd) || _cwd,
944
991
  settingSources: ["user", "project", "local"],
945
992
  abortController: ac,
993
+ settings: { disableAllHooks: true },
946
994
  };
947
995
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
948
996
 
@@ -1102,6 +1150,7 @@ function createClaudeAdapter(opts) {
1102
1150
  if (co.permissionMode) sdkOptions.permissionMode = co.permissionMode;
1103
1151
  if (co.allowDangerouslySkipPermissions) sdkOptions.allowDangerouslySkipPermissions = true;
1104
1152
  if (co.resumeSessionAt) sdkOptions.resumeSessionAt = co.resumeSessionAt;
1153
+ if (co.settings) sdkOptions.settings = co.settings;
1105
1154
 
1106
1155
  var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
1107
1156
  return createQueryHandle(rawQuery, mq, ac);
@@ -1229,7 +1278,7 @@ function createClaudeAdapter(opts) {
1229
1278
  }
1230
1279
 
1231
1280
  // Create the worker query handle (sets up message handler on worker)
1232
- var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation);
1281
+ var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation, queryOpts.callMcpTool);
1233
1282
 
1234
1283
  // Wait for worker to be ready before sending query_start
1235
1284
  if (!reusingWorker) {
@@ -1250,8 +1299,9 @@ function createClaudeAdapter(opts) {
1250
1299
  if (claudeOpts.betas && claudeOpts.betas.length > 0) queryOptions.betas = claudeOpts.betas;
1251
1300
  if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
1252
1301
  if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
1302
+ if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
1253
1303
 
1254
- if (queryOpts.toolServers) queryOptions.mcpServers = queryOpts.toolServers;
1304
+ if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
1255
1305
  if (queryOpts.model) queryOptions.model = queryOpts.model;
1256
1306
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
1257
1307
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
@@ -1356,6 +1406,7 @@ function createClaudeAdapter(opts) {
1356
1406
  cwd: (initOpts && initOpts.cwd) || _cwd,
1357
1407
  settingSources: ["user", "project", "local"],
1358
1408
  abortController: ac,
1409
+ settings: { disableAllHooks: true },
1359
1410
  };
1360
1411
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1361
1412
 
@@ -1430,7 +1481,7 @@ function createClaudeAdapter(opts) {
1430
1481
  throw new Error("Warmup worker failed to connect: " + (e.message || e));
1431
1482
  }
1432
1483
 
1433
- var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"] };
1484
+ var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"], settings: { disableAllHooks: true } };
1434
1485
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1435
1486
  if (initOpts && initOpts.dangerouslySkipPermissions) {
1436
1487
  warmupOptions.permissionMode = "bypassPermissions";
@@ -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";
@@ -164,6 +214,7 @@ function flattenEvent(notification, state) {
164
214
  yokeType: "plan_updated",
165
215
  turnId: params.turnId || null,
166
216
  explanation: params.explanation || "",
217
+ title: "Plan",
167
218
  plan: Array.isArray(params.plan) ? params.plan.map(function(step) {
168
219
  return {
169
220
  step: step && step.step ? step.step : "",
@@ -309,16 +360,33 @@ function flattenEvent(notification, state) {
309
360
  state.thinkingBlocks[item.id] = "blk_" + state.blockCounter;
310
361
  events.push({ yokeType: "thinking_start", blockId: "blk_" + state.blockCounter });
311
362
  }
312
- 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) {
313
381
  var thinkBlockId = state.thinkingBlocks[item.id];
314
382
  var prevThinkLen = state.thinkingLengths[item.id] || 0;
315
- if (item.text.length > prevThinkLen) {
383
+ if (reasoningText.length > prevThinkLen) {
316
384
  events.push({
317
385
  yokeType: "thinking_delta",
318
386
  blockId: thinkBlockId,
319
- text: item.text.substring(prevThinkLen),
387
+ text: reasoningText.substring(prevThinkLen),
320
388
  });
321
- state.thinkingLengths[item.id] = item.text.length;
389
+ state.thinkingLengths[item.id] = reasoningText.length;
322
390
  }
323
391
  }
324
392
  if (evtPhase === "completed") {
@@ -502,6 +570,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
502
570
  var systemPrompt = queryOpts.systemPrompt || "";
503
571
  var canUseTool = queryOpts.canUseTool || null;
504
572
  var onElicitation = queryOpts.onElicitation || null;
573
+ var onFinished = queryOpts.onFinished || null;
505
574
 
506
575
  // Check if the query was cancelled (either via handle.abort() or direct signal abort)
507
576
  function isCancelled() {
@@ -532,6 +601,19 @@ function createCodexQueryHandle(appServer, queryOpts) {
532
601
  var eventBuffer = [];
533
602
  var eventWaiting = null;
534
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
+ }
535
617
 
536
618
  function pushEvent(evt) {
537
619
  if (iteratorDone) return;
@@ -551,6 +633,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
551
633
  eventWaiting = null;
552
634
  resolve({ value: undefined, done: true });
553
635
  }
636
+ notifyFinished();
554
637
  }
555
638
 
556
639
  // Message queue for multi-turn
@@ -710,10 +793,37 @@ function createCodexQueryHandle(appServer, queryOpts) {
710
793
 
711
794
  // --- Main query loop ---
712
795
  async function runQueryLoop(initialMessage) {
713
- // Prepend system prompt (project instructions from YOKE layer) to first message
714
- var currentMessage = systemPrompt
715
- ? systemPrompt + "\n\n" + initialMessage
716
- : initialMessage;
796
+ // Prepend system prompt (project instructions from YOKE layer) to first message.
797
+ // initialMessage may be a string (text-only) or an array of content items
798
+ // (e.g. [{ type: "text", text: "..." }, ...] when images/attachments are present).
799
+ // Naive string concatenation on an array coerces it via toString(), producing
800
+ // "[object Object]" inside the prompt, so we must branch on the shape.
801
+ var currentMessage;
802
+ if (!systemPrompt) {
803
+ currentMessage = initialMessage;
804
+ } else if (typeof initialMessage === "string") {
805
+ currentMessage = systemPrompt + "\n\n" + initialMessage;
806
+ } else if (Array.isArray(initialMessage)) {
807
+ // Prepend systemPrompt to the first text item; if none exists, insert one.
808
+ var cloned = initialMessage.slice();
809
+ var injected = false;
810
+ for (var i = 0; i < cloned.length; i++) {
811
+ if (cloned[i] && cloned[i].type === "text") {
812
+ cloned[i] = {
813
+ type: "text",
814
+ text: systemPrompt + "\n\n" + (cloned[i].text || ""),
815
+ };
816
+ injected = true;
817
+ break;
818
+ }
819
+ }
820
+ if (!injected) {
821
+ cloned.unshift({ type: "text", text: systemPrompt });
822
+ }
823
+ currentMessage = cloned;
824
+ } else {
825
+ currentMessage = initialMessage;
826
+ }
717
827
 
718
828
  try {
719
829
  // Set event handler on app-server
@@ -935,48 +1045,182 @@ function createCodexQueryHandle(appServer, queryOpts) {
935
1045
 
936
1046
  function createCodexAdapter(opts) {
937
1047
  var _cwd = (opts && opts.cwd) || process.cwd();
1048
+ var _slug = (opts && opts.slug) || "";
1049
+ var _defaultInitOpts = Object.assign({}, opts || {});
938
1050
  var _cachedModels = [];
939
1051
  var _appServer = null;
940
1052
  var _initPromise = null;
941
- var _initOpts = null; // stored for query-time access
1053
+ var _shutdownPromise = null;
1054
+ var _refCount = 0;
1055
+ var _lastActiveAt = Date.now();
1056
+ var _shuttingDown = false;
1057
+ var _activeQueries = [];
1058
+
1059
+ function updateLastActiveAt() {
1060
+ _lastActiveAt = Date.now();
1061
+ }
1062
+
1063
+ function registerActiveQuery(entry) {
1064
+ _activeQueries.push(entry);
1065
+ }
1066
+
1067
+ function removeActiveQuery(entry) {
1068
+ var next = [];
1069
+ for (var i = 0; i < _activeQueries.length; i++) {
1070
+ if (_activeQueries[i] !== entry) next.push(_activeQueries[i]);
1071
+ }
1072
+ _activeQueries = next;
1073
+ }
1074
+
1075
+ function decrementRefCount() {
1076
+ if (_refCount > 0) {
1077
+ _refCount--;
1078
+ } else {
1079
+ console.error("[yoke/codex] refCount negative, bug!");
1080
+ _refCount = 0;
1081
+ }
1082
+ updateLastActiveAt();
1083
+ }
1084
+
1085
+ function buildReadyResponse(skillNames) {
1086
+ return {
1087
+ models: _cachedModels,
1088
+ defaultModel: "gpt-5.4",
1089
+ skills: skillNames || [],
1090
+ slashCommands: skillNames || [],
1091
+ fastModeState: null,
1092
+ capabilities: {
1093
+ thinking: true,
1094
+ betas: false,
1095
+ rewind: false,
1096
+ sessionResume: true,
1097
+ promptSuggestions: true,
1098
+ elicitation: true,
1099
+ fileCheckpointing: false,
1100
+ contextCompacting: false,
1101
+ toolPolicy: ["ask", "allow-all"],
1102
+ },
1103
+ };
1104
+ }
1105
+
1106
+ function clearRuntimeState() {
1107
+ _appServer = null;
1108
+ _initPromise = null;
1109
+ _cachedModels = [];
1110
+ _refCount = 0;
1111
+ _activeQueries = [];
1112
+ updateLastActiveAt();
1113
+ }
1114
+
1115
+ function waitForRefCount(targetCount, timeoutMs) {
1116
+ var deadline = Date.now() + (timeoutMs || 5000);
1117
+ return new Promise(function(resolve) {
1118
+ function tick() {
1119
+ if (_refCount <= targetCount) {
1120
+ resolve(true);
1121
+ return;
1122
+ }
1123
+ if (Date.now() >= deadline) {
1124
+ resolve(false);
1125
+ return;
1126
+ }
1127
+ setTimeout(tick, 50);
1128
+ }
1129
+ tick();
1130
+ });
1131
+ }
1132
+
1133
+ function stopAppServer(deadlineMs) {
1134
+ var proc = _appServer && _appServer.proc ? _appServer.proc : null;
1135
+ if (!_appServer) return Promise.resolve(true);
1136
+ try {
1137
+ _appServer.stop();
1138
+ } catch (e) {
1139
+ console.error("[yoke/codex] App-server stop error:", e.message || e);
1140
+ }
1141
+ if (!proc) return Promise.resolve(true);
1142
+ var remaining = (typeof deadlineMs === "number") ? Math.max(0, deadlineMs - Date.now()) : 5000;
1143
+ return waitForProcessExit(proc, remaining).then(function(exited) {
1144
+ if (!exited) {
1145
+ try {
1146
+ proc.kill("SIGKILL");
1147
+ } catch (e) {}
1148
+ }
1149
+ return exited;
1150
+ });
1151
+ }
1152
+
1153
+ function beginShutdown(force, idleMs) {
1154
+ if (_shutdownPromise) return _shutdownPromise;
1155
+ if (_shuttingDown) return null;
1156
+
1157
+ _shuttingDown = true;
1158
+
1159
+ _shutdownPromise = (async function() {
1160
+ var deadline = Date.now() + 5000;
1161
+ var shouldAbort = !!force;
1162
+
1163
+ if (_initPromise) {
1164
+ try {
1165
+ await Promise.race([
1166
+ _initPromise.catch(function() { return null; }),
1167
+ waitMs(Math.max(0, deadline - Date.now())),
1168
+ ]);
1169
+ } catch (e) {}
1170
+ }
1171
+
1172
+ if (shouldAbort && _activeQueries.length > 0) {
1173
+ var active = _activeQueries.slice();
1174
+ for (var i = 0; i < active.length; i++) {
1175
+ try {
1176
+ if (active[i] && active[i].abort) active[i].abort();
1177
+ } catch (e) {}
1178
+ }
1179
+ await waitForRefCount(0, Math.max(0, deadline - Date.now()));
1180
+ }
1181
+
1182
+ if (_appServer) {
1183
+ await stopAppServer(deadline);
1184
+ }
1185
+
1186
+ clearRuntimeState();
1187
+ _shuttingDown = false;
1188
+ _shutdownPromise = null;
1189
+ return true;
1190
+ })().catch(function(err) {
1191
+ clearRuntimeState();
1192
+ _shuttingDown = false;
1193
+ _shutdownPromise = null;
1194
+ throw err;
1195
+ });
1196
+
1197
+ return _shutdownPromise;
1198
+ }
942
1199
 
943
1200
  var adapter = {
944
1201
  vendor: "codex",
945
1202
 
946
1203
  init: function(initOpts) {
1204
+ if (_shuttingDown) {
1205
+ return Promise.reject(createShutdownError());
1206
+ }
1207
+
1208
+ var effectiveInitOpts = Object.assign({}, _defaultInitOpts, initOpts || {});
1209
+
947
1210
  // Already initialized - return cached result
948
1211
  if (_appServer && _appServer.started && _cachedModels.length > 0) {
949
- return Promise.resolve({
950
- models: _cachedModels,
951
- defaultModel: "gpt-5.4",
952
- skills: [],
953
- slashCommands: [],
954
- fastModeState: null,
955
- capabilities: {
956
- thinking: true,
957
- betas: false,
958
- rewind: false,
959
- sessionResume: true,
960
- promptSuggestions: true,
961
- elicitation: true,
962
- fileCheckpointing: false,
963
- contextCompacting: false,
964
- toolPolicy: ["ask", "allow-all"],
965
- },
966
- });
1212
+ return Promise.resolve(buildReadyResponse([]));
967
1213
  }
968
1214
 
969
1215
  // Deduplicate concurrent init calls
970
1216
  if (_initPromise) return _initPromise;
971
1217
 
972
1218
  _initPromise = (async function() {
973
- _initOpts = initOpts;
974
-
975
1219
  var serverOpts = { cwd: _cwd };
976
1220
 
977
1221
  // Extract adapter options
978
- if (initOpts && initOpts.adapterOptions && initOpts.adapterOptions.CODEX) {
979
- var co = initOpts.adapterOptions.CODEX;
1222
+ if (effectiveInitOpts && effectiveInitOpts.adapterOptions && effectiveInitOpts.adapterOptions.CODEX) {
1223
+ var co = effectiveInitOpts.adapterOptions.CODEX;
980
1224
  if (co.apiKey) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_API_KEY: co.apiKey });
981
1225
  if (co.baseUrl) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_BASE_URL: co.baseUrl });
982
1226
  if (co.config) serverOpts.config = co.config;
@@ -1004,10 +1248,10 @@ function createCodexAdapter(opts) {
1004
1248
 
1005
1249
  // Track 2: Add clay-tools bridge server for in-app + remote MCP tools.
1006
1250
  var bridgePath = require("path").join(__dirname, "..", "mcp-bridge-server.js");
1007
- var clayPort = (initOpts && initOpts.clayPort) || process.env.CLAY_PORT || 2633;
1008
- var clayTls = (initOpts && initOpts.clayTls) || false;
1009
- var clayAuthToken = (initOpts && initOpts.clayAuthToken) || "";
1010
- var claySlug = (initOpts && initOpts.slug) || "";
1251
+ var clayPort = effectiveInitOpts.clayPort || process.env.CLAY_PORT || 2633;
1252
+ var clayTls = effectiveInitOpts.clayTls || false;
1253
+ var clayAuthToken = effectiveInitOpts.clayAuthToken || "";
1254
+ var claySlug = effectiveInitOpts.slug || _slug || "";
1011
1255
  try {
1012
1256
  if (require("fs").existsSync(bridgePath)) {
1013
1257
  var bridgeArgs = [bridgePath, "--port", String(clayPort), "--slug", claySlug];
@@ -1048,6 +1292,11 @@ function createCodexAdapter(opts) {
1048
1292
  });
1049
1293
  _appServer.notify("initialized", {});
1050
1294
 
1295
+ if (_shuttingDown) {
1296
+ await stopAppServer(Date.now() + 1000);
1297
+ throw createShutdownError();
1298
+ }
1299
+
1051
1300
  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");
1052
1301
 
1053
1302
  _cachedModels = [
@@ -1096,26 +1345,15 @@ function createCodexAdapter(opts) {
1096
1345
  console.error("[codex] Failed to discover skills:", e.message);
1097
1346
  }
1098
1347
 
1348
+ if (_shuttingDown) {
1349
+ await stopAppServer(Date.now() + 1000);
1350
+ throw createShutdownError();
1351
+ }
1352
+
1099
1353
  _initPromise = null;
1354
+ updateLastActiveAt();
1100
1355
 
1101
- return {
1102
- models: _cachedModels,
1103
- defaultModel: "gpt-5.4",
1104
- skills: skillNames,
1105
- slashCommands: skillNames,
1106
- fastModeState: null,
1107
- capabilities: {
1108
- thinking: true,
1109
- betas: false,
1110
- rewind: false,
1111
- sessionResume: true,
1112
- promptSuggestions: true,
1113
- elicitation: true,
1114
- fileCheckpointing: false,
1115
- contextCompacting: false,
1116
- toolPolicy: ["ask", "allow-all"],
1117
- },
1118
- };
1356
+ return buildReadyResponse(skillNames);
1119
1357
  })();
1120
1358
 
1121
1359
  return _initPromise;
@@ -1133,12 +1371,31 @@ function createCodexAdapter(opts) {
1133
1371
  },
1134
1372
 
1135
1373
  createQuery: async function(queryOpts) {
1374
+ if (_shuttingDown) {
1375
+ throw createShutdownError();
1376
+ }
1377
+
1378
+ if (!_appServer || !_appServer.started) {
1379
+ await adapter.init(queryOpts || {});
1380
+ }
1381
+
1382
+ if (_shuttingDown) {
1383
+ throw createShutdownError();
1384
+ }
1385
+
1136
1386
  if (!_appServer || !_appServer.started) {
1137
1387
  throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
1138
1388
  }
1139
1389
 
1140
1390
  var model = queryOpts.model || "gpt-5.4";
1141
1391
  var ac = queryOpts.abortController || new AbortController();
1392
+ var activeEntry = {
1393
+ abort: function() {
1394
+ try {
1395
+ ac.abort();
1396
+ } catch (e) {}
1397
+ },
1398
+ };
1142
1399
 
1143
1400
  // Map YOKE options to Codex thread options
1144
1401
  var codexOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.CODEX) || {};
@@ -1175,7 +1432,33 @@ function createCodexAdapter(opts) {
1175
1432
 
1176
1433
  console.log("[yoke/codex] createQuery: model=" + model + " approval=" + handleOpts.approvalPolicy + " sandbox=" + handleOpts.sandboxMode);
1177
1434
 
1178
- var handle = createCodexQueryHandle(_appServer, handleOpts);
1435
+ _refCount++;
1436
+ registerActiveQuery(activeEntry);
1437
+
1438
+ var handle;
1439
+ try {
1440
+ handleOpts.onFinished = function() {
1441
+ removeActiveQuery(activeEntry);
1442
+ decrementRefCount();
1443
+ };
1444
+ handle = createCodexQueryHandle(_appServer, handleOpts);
1445
+ } catch (e) {
1446
+ removeActiveQuery(activeEntry);
1447
+ decrementRefCount();
1448
+ throw e;
1449
+ }
1450
+
1451
+ activeEntry.handle = handle;
1452
+ activeEntry.abort = function() {
1453
+ try {
1454
+ if (handle && typeof handle.abort === "function") {
1455
+ handle.abort();
1456
+ } else {
1457
+ ac.abort();
1458
+ }
1459
+ } catch (e) {}
1460
+ };
1461
+
1179
1462
  return handle;
1180
1463
  },
1181
1464
 
@@ -1242,10 +1525,19 @@ function createCodexAdapter(opts) {
1242
1525
 
1243
1526
  // Shutdown the app-server process
1244
1527
  shutdown: function() {
1245
- if (_appServer) {
1246
- _appServer.stop();
1247
- _appServer = null;
1248
- }
1528
+ return beginShutdown(true);
1529
+ },
1530
+
1531
+ shutdownIfIdle: function(idleMs) {
1532
+ if (_shuttingDown || _shutdownPromise) return Promise.resolve(false);
1533
+ if (_initPromise) return Promise.resolve(false);
1534
+ if (!_appServer) return Promise.resolve(false);
1535
+ if (_refCount > 0) return Promise.resolve(false);
1536
+ if (Date.now() - _lastActiveAt < (idleMs || 0)) return Promise.resolve(false);
1537
+ return beginShutdown(false).then(function() {
1538
+ console.log("[yoke/codex] Reclaimed idle adapter for project " + (_slug || _cwd));
1539
+ return true;
1540
+ });
1249
1541
  },
1250
1542
  };
1251
1543