clay-server 4.7.1-beta.4 → 4.7.1-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.
@@ -14,6 +14,7 @@ var { resolveOsUserInfo } = require("../../os-users");
14
14
  var backgroundTasks = require("../codex-background-tasks");
15
15
  var codexUserInput = require("../codex-user-input");
16
16
  var createTurnStop = require("../codex-turn-stop").createTurnStop;
17
+ var idleRecovery = require("../codex-idle-recovery");
17
18
 
18
19
  // --- Reasoning defaults ---
19
20
  // Reasoning summaries are the only readable thinking channel Codex offers:
@@ -895,6 +896,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
895
896
  var messageWaiting = null;
896
897
  var messageQueueEnded = false;
897
898
  var handlerEntry = null;
899
+ var inputRevision = 0;
900
+ var resolveLoopFinished;
901
+ var loopFinished = new Promise(function(resolve) { resolveLoopFinished = resolve; });
898
902
 
899
903
  function pushMessageToQueue(msg) {
900
904
  if (messageQueueEnded) return false;
@@ -1314,6 +1318,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
1314
1318
 
1315
1319
  state.done = true;
1316
1320
  endIterator();
1321
+ resolveLoopFinished();
1317
1322
  }
1318
1323
 
1319
1324
  var handle = {
@@ -1333,6 +1338,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
1333
1338
 
1334
1339
  pushMessage: function(text, images) {
1335
1340
  if (iteratorDone || state.done || messageQueueEnded) return false;
1341
+ inputRevision++;
1336
1342
  var input;
1337
1343
  if (images && images.length > 0) {
1338
1344
  input = [];
@@ -1354,6 +1360,16 @@ function createCodexQueryHandle(appServer, queryOpts) {
1354
1360
  }
1355
1361
  },
1356
1362
 
1363
+ getIdleRecoverySnapshot: function() {
1364
+ if (!state.threadId || !messageWaiting || messageQueue.length || messageQueueEnded || iteratorDone || isCancelled()) return null;
1365
+ return { threadId: state.threadId, revision: inputRevision };
1366
+ },
1367
+
1368
+ retireIdleForRecovery: function() {
1369
+ handle.endInput();
1370
+ return loopFinished;
1371
+ },
1372
+
1357
1373
  setModel: function(model) {
1358
1374
  state.model = model || DEFAULT_CODEX_MODEL;
1359
1375
  return Promise.resolve();
@@ -1477,6 +1493,7 @@ function createCodexAdapter(opts) {
1477
1493
  var _initPromise = null;
1478
1494
  var _initializing = false;
1479
1495
  var _shutdownPromise = null;
1496
+ var _shutdownEpoch = 0;
1480
1497
  var _refCount = 0;
1481
1498
  var _lastActiveAt = Date.now();
1482
1499
  var _shuttingDown = false;
@@ -1560,10 +1577,6 @@ function createCodexAdapter(opts) {
1560
1577
  updateLastActiveAt();
1561
1578
  }
1562
1579
 
1563
- function waitForRecoveryHandoff() {
1564
- return _recoveryHandoff || Promise.resolve();
1565
- }
1566
-
1567
1580
  function retainRecoveryRuntime(runtime) {
1568
1581
  _recoveryRuntimes.push(runtime);
1569
1582
  }
@@ -1612,6 +1625,11 @@ function createCodexAdapter(opts) {
1612
1625
  }
1613
1626
 
1614
1627
  async function createIsolatedRecoveryQuery(queryOpts) {
1628
+ var shutdownEpoch = _shutdownEpoch;
1629
+ function checkCurrent() {
1630
+ idleRecovery.checkAbort(queryOpts);
1631
+ if (_shuttingDown || shutdownEpoch !== _shutdownEpoch) throw createShutdownError();
1632
+ }
1615
1633
  var releaseHandoff;
1616
1634
  var priorHandoff = _recoveryHandoff;
1617
1635
  var myHandoff = new Promise(function(resolve) { releaseHandoff = resolve; });
@@ -1619,6 +1637,11 @@ function createCodexAdapter(opts) {
1619
1637
  await (priorHandoff || Promise.resolve());
1620
1638
  try {
1621
1639
  await _recoveryCleanup;
1640
+ checkCurrent();
1641
+ if (!_initializing && _refCount > 0) {
1642
+ await idleRecovery.retireIdleQueries(_activeQueries.slice(), _appServer, checkCurrent);
1643
+ }
1644
+ checkCurrent();
1622
1645
  if (_refCount > 0 || _initializing) {
1623
1646
  throw new Error("Codex resumed-tool recovery is waiting for an active shared runtime; retry after its current queries finish.");
1624
1647
  }
@@ -1626,6 +1649,7 @@ function createCodexAdapter(opts) {
1626
1649
  var stopped = await adapter.shutdownIfIdle(0);
1627
1650
  if (!stopped) throw new Error("Codex resumed-tool recovery could not safely release the shared runtime; retry after its current queries finish.");
1628
1651
  }
1652
+ checkCurrent();
1629
1653
  var recoveryRuntime = createCodexAdapter(Object.assign({}, _defaultInitOpts, {
1630
1654
  recoveryRuntime: true,
1631
1655
  recoverySessionMcpServer: queryOpts.sessionMcpServer,
@@ -1792,6 +1816,13 @@ function createCodexAdapter(opts) {
1792
1816
  if (_shuttingDown) {
1793
1817
  return Promise.reject(createShutdownError());
1794
1818
  }
1819
+ if (_recoveryHandoff) {
1820
+ var initEpoch = _shutdownEpoch;
1821
+ return _recoveryHandoff.then(function() {
1822
+ if (initEpoch !== _shutdownEpoch) throw createShutdownError();
1823
+ return adapter.init(initOpts);
1824
+ });
1825
+ }
1795
1826
 
1796
1827
  var effectiveInitOpts = Object.assign({}, _defaultInitOpts, initOpts || {});
1797
1828
 
@@ -2004,10 +2035,25 @@ function createCodexAdapter(opts) {
2004
2035
  if (queryOpts && queryOpts.sessionMcpServer && queryOpts.resumeSessionId && !queryOpts.isolatedRecovery) {
2005
2036
  return createIsolatedRecoveryQuery(queryOpts);
2006
2037
  }
2007
- await waitForRecoveryHandoff();
2008
-
2009
- if (!_appServer || !_appServer.started) {
2010
- await adapter.init(queryOpts || {});
2038
+ var queryEpoch = _shutdownEpoch;
2039
+ // Recheck after every await: recovery may replace the runtime while an
2040
+ // ordinary start is waiting for initialization or an earlier handoff.
2041
+ while (true) {
2042
+ idleRecovery.checkAbort(queryOpts || {});
2043
+ if (_shuttingDown || queryEpoch !== _shutdownEpoch) throw createShutdownError();
2044
+ if (_recoveryHandoff) {
2045
+ await _recoveryHandoff;
2046
+ continue;
2047
+ }
2048
+ if (_initializing && _initPromise) {
2049
+ await _initPromise;
2050
+ continue;
2051
+ }
2052
+ if (!_appServer || !_appServer.started) {
2053
+ await adapter.init(queryOpts || {});
2054
+ continue;
2055
+ }
2056
+ break;
2011
2057
  }
2012
2058
 
2013
2059
  if (_shuttingDown) {
@@ -2186,6 +2232,7 @@ function createCodexAdapter(opts) {
2186
2232
 
2187
2233
  // Shutdown the app-server process
2188
2234
  shutdown: function() {
2235
+ _shutdownEpoch++;
2189
2236
  return Promise.all([
2190
2237
  beginShutdown(true),
2191
2238
  shutdownUserRuntimes("shutdown"),
@@ -0,0 +1,52 @@
1
+ // Retire retained inputs only after every query is verified idle. The adapter
2
+ // holds admission closed while this runs; existing handles may still receive
3
+ // messages, so their input revisions must be checked again after each probe.
4
+ function checkAbort(queryOpts) {
5
+ var signal = queryOpts.abortController && queryOpts.abortController.signal;
6
+ if (signal && signal.aborted) {
7
+ var error = new Error("Query cancelled");
8
+ error.name = "AbortError";
9
+ throw error;
10
+ }
11
+ }
12
+
13
+ function terminalsAreIdle(result) {
14
+ if (!result || result.nextCursor) return false;
15
+ var terminals = Array.isArray(result) ? result
16
+ : result.data || result.terminals || result.backgroundTerminals || result.items;
17
+ if (!Array.isArray(terminals)) return false;
18
+ return terminals.every(function(terminal) {
19
+ var status = String(terminal && (terminal.status || terminal.state) || "").toLowerCase();
20
+ return /^(exited|terminated|completed|failed)$/.test(status);
21
+ });
22
+ }
23
+
24
+ async function retireIdleQueries(entries, server, checkCurrent) {
25
+ checkCurrent();
26
+ var candidates = [];
27
+ for (var i = 0; i < entries.length; i++) {
28
+ var handle = entries[i].handle;
29
+ var snapshot = handle && handle.getIdleRecoverySnapshot && handle.getIdleRecoverySnapshot();
30
+ if (!snapshot) return false;
31
+ candidates.push({ handle: handle, snapshot: snapshot });
32
+ }
33
+ var safe = await Promise.all(candidates.map(function(candidate) {
34
+ return server.send("thread/backgroundTerminals/list", {
35
+ threadId: candidate.snapshot.threadId,
36
+ }, 5000).then(terminalsAreIdle, function() { return false; });
37
+ }));
38
+ checkCurrent();
39
+ for (var ci = 0; ci < candidates.length; ci++) {
40
+ var current = candidates[ci].handle.getIdleRecoverySnapshot();
41
+ if (!safe[ci] || !current || current.revision !== candidates[ci].snapshot.revision) return false;
42
+ }
43
+ // No awaits between validation and closing every input. A new send either
44
+ // already invalidated its snapshot or sees a closed input and resumes anew.
45
+ var finished = candidates.map(function(candidate) {
46
+ return candidate.handle.retireIdleForRecovery();
47
+ });
48
+ await Promise.all(finished);
49
+ return true;
50
+ }
51
+
52
+ module.exports = { retireIdleQueries: retireIdleQueries, checkAbort: checkAbort };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.7.1-beta.4",
3
+ "version": "4.7.1-beta.5",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"