very-happy-cli 0.2.136 → 0.2.138

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 (43) hide show
  1. package/dist/{AcpBackend-CRzu0_1Z.cjs → AcpBackend-BUoDXZnv.cjs} +2 -2
  2. package/dist/{AcpBackend-C142vP3p.mjs → AcpBackend-bqVGMkPh.mjs} +2 -2
  3. package/dist/{AcpSessionManager-CosTbAlR.mjs → AcpSessionManager-BBAQ8EQf.mjs} +1 -1
  4. package/dist/{AcpSessionManager-Ba6WMLXc.cjs → AcpSessionManager-DmmA52Ep.cjs} +1 -1
  5. package/dist/{agentGuidance-6yh_wPwD.cjs → agentGuidance-4HV-BOge.cjs} +1 -1
  6. package/dist/{agentGuidance-ASxKHxBP.mjs → agentGuidance-WHZsBKpm.mjs} +1 -1
  7. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  8. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  9. package/dist/{config-VqHQ67d8.mjs → config-BRXqgJJj.mjs} +4 -4
  10. package/dist/{config--cxxmkOd.cjs → config-DpuZN2Rs.cjs} +4 -4
  11. package/dist/{index-BPVQFcKW.cjs → index-3N-qu-RG.cjs} +6 -6
  12. package/dist/{index-BqbtKX0T.cjs → index-CHP265gs.cjs} +4 -4
  13. package/dist/{index-8bwU9d-4.cjs → index-CRRK5P1r.cjs} +115 -23
  14. package/dist/{index-vN4Xf0zB.mjs → index-D8Q3OhdR.mjs} +4 -4
  15. package/dist/{index-D3vlFjXy.mjs → index-DQe7ljLR.mjs} +7 -7
  16. package/dist/{index-CHI7iTa0.mjs → index-Dbl5U_N0.mjs} +111 -19
  17. package/dist/index.cjs +4 -4
  18. package/dist/index.mjs +4 -4
  19. package/dist/{installTerminalHooks-2POBqVlu.cjs → installTerminalHooks-CWGkoWDD.cjs} +4 -4
  20. package/dist/{installTerminalHooks-D0YRu_p-.mjs → installTerminalHooks-DfUGgeyb.mjs} +4 -4
  21. package/dist/lib.cjs +2 -2
  22. package/dist/lib.mjs +2 -2
  23. package/dist/{limits-DRrdJ7gr.cjs → limits-CfzVummd.cjs} +7 -7
  24. package/dist/{limits-kjHZUlSp.mjs → limits-DEsg_gHi.mjs} +7 -7
  25. package/dist/{mcp-C1JQsM1s.cjs → mcp-CN2UVfut.cjs} +4 -4
  26. package/dist/{mcp-CgjEHlky.mjs → mcp-UnjB3I8_.mjs} +4 -4
  27. package/dist/{runGemini-B9Rc-H0G.cjs → runGemini-BUVjTJKn.cjs} +6 -6
  28. package/dist/{runGemini-DufGnnl2.mjs → runGemini-BZ0xXRj-.mjs} +6 -6
  29. package/dist/{runOpenClaw-DAl2nnjB.mjs → runOpenClaw-BcUmnBpW.mjs} +5 -5
  30. package/dist/{runOpenClaw-BsRRKDvF.cjs → runOpenClaw-Bj2EgRHB.cjs} +5 -5
  31. package/dist/{send-DP6n2J9x.cjs → send-BkypdBpK.cjs} +4 -4
  32. package/dist/{send-BHquPaeG.mjs → send-snK_NGAA.mjs} +4 -4
  33. package/dist/{sessions-Q4BR02dG.cjs → sessions-9A-uoJL_.cjs} +4 -4
  34. package/dist/{sessions-BtrLzv5U.mjs → sessions-DxlC4C4g.mjs} +4 -4
  35. package/dist/{spawn-CMjhlwlD.cjs → spawn-BORLpj60.cjs} +4 -4
  36. package/dist/{spawn-DuQXydqI.mjs → spawn-DFfHCafP.mjs} +4 -4
  37. package/dist/{teams-CkdUH10-.cjs → teams-9x2FO9RW.cjs} +4 -4
  38. package/dist/{teams-D5HxnyaW.mjs → teams-Cfr-2Yd-.mjs} +4 -4
  39. package/dist/{todo-BiLIZH4t.mjs → todo-CU5cjQZU.mjs} +3 -3
  40. package/dist/{todo-CaYedmtq.cjs → todo-DydKcknX.cjs} +3 -3
  41. package/dist/{types-DbBPNIwf.cjs → types-CRxf_3n8.cjs} +78 -6
  42. package/dist/{types-BKrZubg8.mjs → types-D5EsIAOe.mjs} +77 -5
  43. package/package.json +7 -7
@@ -3,15 +3,15 @@
3
3
  var chalk = require('chalk');
4
4
  var node_fs = require('node:fs');
5
5
  var path = require('node:path');
6
- var persistence = require('./limits-DRrdJ7gr.cjs');
7
- var index = require('./index-8bwU9d-4.cjs');
6
+ var persistence = require('./limits-CfzVummd.cjs');
7
+ var index = require('./index-CRRK5P1r.cjs');
8
8
  require('node:fs/promises');
9
9
  require('node:os');
10
10
  require('zod');
11
11
  require('node:crypto');
12
12
  require('tweetnacl');
13
13
  require('node:util');
14
- require('./types-DbBPNIwf.cjs');
14
+ require('./types-CRxf_3n8.cjs');
15
15
  require('axios');
16
16
  require('node:events');
17
17
  require('socket.io-client');
@@ -30,7 +30,7 @@ require('fs');
30
30
  require('node:readline');
31
31
  require('@anthropic-ai/sandbox-runtime');
32
32
  require('expo-server-sdk');
33
- require('./agentGuidance-6yh_wPwD.cjs');
33
+ require('./agentGuidance-4HV-BOge.cjs');
34
34
  require('ink');
35
35
  require('react');
36
36
  require('@anthropic-ai/claude-agent-sdk');
@@ -1,15 +1,15 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { r as readPersistedSessions, c as configuration } from './limits-kjHZUlSp.mjs';
5
- import { d as sendUserMessage, a as sessionWebUrl } from './index-CHI7iTa0.mjs';
4
+ import { r as readPersistedSessions, c as configuration } from './limits-DEsg_gHi.mjs';
5
+ import { d as sendUserMessage, a as sessionWebUrl } from './index-Dbl5U_N0.mjs';
6
6
  import 'node:fs/promises';
7
7
  import 'node:os';
8
8
  import 'zod';
9
9
  import 'node:crypto';
10
10
  import 'tweetnacl';
11
11
  import 'node:util';
12
- import './types-BKrZubg8.mjs';
12
+ import './types-D5EsIAOe.mjs';
13
13
  import 'axios';
14
14
  import 'node:events';
15
15
  import 'socket.io-client';
@@ -28,7 +28,7 @@ import 'fs';
28
28
  import 'node:readline';
29
29
  import '@anthropic-ai/sandbox-runtime';
30
30
  import 'expo-server-sdk';
31
- import './agentGuidance-ASxKHxBP.mjs';
31
+ import './agentGuidance-WHZsBKpm.mjs';
32
32
  import 'ink';
33
33
  import 'react';
34
34
  import '@anthropic-ai/claude-agent-sdk';
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var index = require('./index-8bwU9d-4.cjs');
5
- var api = require('./types-DbBPNIwf.cjs');
4
+ var index = require('./index-CRRK5P1r.cjs');
5
+ var api = require('./types-CRxf_3n8.cjs');
6
6
  require('node:os');
7
7
  require('node:crypto');
8
- require('./limits-DRrdJ7gr.cjs');
8
+ require('./limits-CfzVummd.cjs');
9
9
  require('node:fs/promises');
10
10
  require('node:fs');
11
11
  require('node:path');
@@ -14,7 +14,7 @@ require('tweetnacl');
14
14
  require('node:util');
15
15
  require('cross-spawn');
16
16
  require('node:readline');
17
- require('./agentGuidance-6yh_wPwD.cjs');
17
+ require('./agentGuidance-4HV-BOge.cjs');
18
18
  require('fs/promises');
19
19
  require('ink');
20
20
  require('react');
@@ -1,9 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, e as listSessions, r as readSessionTranscript, f as stopSession, g as resolvePermissionRequest, h as archiveSession } from './index-CHI7iTa0.mjs';
3
- import { i as isValidSessionId } from './types-BKrZubg8.mjs';
2
+ import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, e as listSessions, r as readSessionTranscript, f as stopSession, g as resolvePermissionRequest, h as archiveSession } from './index-Dbl5U_N0.mjs';
3
+ import { i as isValidSessionId } from './types-D5EsIAOe.mjs';
4
4
  import 'node:os';
5
5
  import 'node:crypto';
6
- import './limits-kjHZUlSp.mjs';
6
+ import './limits-DEsg_gHi.mjs';
7
7
  import 'node:fs/promises';
8
8
  import 'node:fs';
9
9
  import 'node:path';
@@ -12,7 +12,7 @@ import 'tweetnacl';
12
12
  import 'node:util';
13
13
  import 'cross-spawn';
14
14
  import 'node:readline';
15
- import './agentGuidance-ASxKHxBP.mjs';
15
+ import './agentGuidance-WHZsBKpm.mjs';
16
16
  import 'fs/promises';
17
17
  import 'ink';
18
18
  import 'react';
@@ -3,11 +3,11 @@
3
3
  var chalk = require('chalk');
4
4
  var node_fs = require('node:fs');
5
5
  var path = require('node:path');
6
- var index = require('./index-8bwU9d-4.cjs');
7
- var persistence = require('./limits-DRrdJ7gr.cjs');
6
+ var index = require('./index-CRRK5P1r.cjs');
7
+ var persistence = require('./limits-CfzVummd.cjs');
8
8
  require('node:os');
9
9
  require('node:crypto');
10
- require('./types-DbBPNIwf.cjs');
10
+ require('./types-CRxf_3n8.cjs');
11
11
  require('axios');
12
12
  require('node:events');
13
13
  require('socket.io-client');
@@ -29,7 +29,7 @@ require('tweetnacl');
29
29
  require('node:readline');
30
30
  require('@anthropic-ai/sandbox-runtime');
31
31
  require('expo-server-sdk');
32
- require('./agentGuidance-6yh_wPwD.cjs');
32
+ require('./agentGuidance-4HV-BOge.cjs');
33
33
  require('ink');
34
34
  require('react');
35
35
  require('@anthropic-ai/claude-agent-sdk');
@@ -1,11 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, w as waitForSessionKey, d as sendUserMessage } from './index-CHI7iTa0.mjs';
5
- import { l as logger } from './limits-kjHZUlSp.mjs';
4
+ import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, w as waitForSessionKey, d as sendUserMessage } from './index-Dbl5U_N0.mjs';
5
+ import { l as logger } from './limits-DEsg_gHi.mjs';
6
6
  import 'node:os';
7
7
  import 'node:crypto';
8
- import './types-BKrZubg8.mjs';
8
+ import './types-D5EsIAOe.mjs';
9
9
  import 'axios';
10
10
  import 'node:events';
11
11
  import 'socket.io-client';
@@ -27,7 +27,7 @@ import 'tweetnacl';
27
27
  import 'node:readline';
28
28
  import '@anthropic-ai/sandbox-runtime';
29
29
  import 'expo-server-sdk';
30
- import './agentGuidance-ASxKHxBP.mjs';
30
+ import './agentGuidance-WHZsBKpm.mjs';
31
31
  import 'ink';
32
32
  import 'react';
33
33
  import '@anthropic-ai/claude-agent-sdk';
@@ -4,15 +4,15 @@ var os = require('node:os');
4
4
  var promises = require('node:fs/promises');
5
5
  var z = require('zod');
6
6
  var node_crypto = require('node:crypto');
7
- var agentGuidance = require('./agentGuidance-6yh_wPwD.cjs');
8
- var persistence = require('./limits-DRrdJ7gr.cjs');
7
+ var agentGuidance = require('./agentGuidance-4HV-BOge.cjs');
8
+ var persistence = require('./limits-CfzVummd.cjs');
9
9
  var path = require('node:path');
10
- var index = require('./index-8bwU9d-4.cjs');
10
+ var index = require('./index-CRRK5P1r.cjs');
11
11
  require('node:fs');
12
12
  require('tweetnacl');
13
13
  require('chalk');
14
14
  require('node:util');
15
- require('./types-DbBPNIwf.cjs');
15
+ require('./types-CRxf_3n8.cjs');
16
16
  require('axios');
17
17
  require('node:events');
18
18
  require('socket.io-client');
@@ -2,15 +2,15 @@ import { homedir } from 'node:os';
2
2
  import { unlink, rmdir, mkdir, lstat, readFile, writeFile, rename } from 'node:fs/promises';
3
3
  import { z } from 'zod';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
- import { d as teamRequest, e as createTeamsClient } from './agentGuidance-ASxKHxBP.mjs';
6
- import { d as readCredentialsForConfiguredRelay } from './limits-kjHZUlSp.mjs';
5
+ import { d as teamRequest, e as createTeamsClient } from './agentGuidance-WHZsBKpm.mjs';
6
+ import { d as readCredentialsForConfiguredRelay } from './limits-DEsg_gHi.mjs';
7
7
  import { join, resolve, dirname } from 'node:path';
8
- import { T as TEAM_SKILL } from './index-CHI7iTa0.mjs';
8
+ import { T as TEAM_SKILL } from './index-Dbl5U_N0.mjs';
9
9
  import 'node:fs';
10
10
  import 'tweetnacl';
11
11
  import 'chalk';
12
12
  import 'node:util';
13
- import './types-BKrZubg8.mjs';
13
+ import './types-D5EsIAOe.mjs';
14
14
  import 'axios';
15
15
  import 'node:events';
16
16
  import 'socket.io-client';
@@ -1,6 +1,6 @@
1
- import { d as readCredentialsForConfiguredRelay, c as configuration } from './limits-kjHZUlSp.mjs';
2
- import { b as createBuiltinTodoClient, B as BUILTIN_TODO_SKILL } from './types-BKrZubg8.mjs';
3
- import { c as readTeamScope, t as teamScopePath } from './agentGuidance-ASxKHxBP.mjs';
1
+ import { d as readCredentialsForConfiguredRelay, c as configuration } from './limits-DEsg_gHi.mjs';
2
+ import { b as createBuiltinTodoClient, B as BUILTIN_TODO_SKILL } from './types-D5EsIAOe.mjs';
3
+ import { c as readTeamScope, t as teamScopePath } from './agentGuidance-WHZsBKpm.mjs';
4
4
  import 'node:fs/promises';
5
5
  import 'node:fs';
6
6
  import 'node:os';
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var persistence = require('./limits-DRrdJ7gr.cjs');
4
- var api = require('./types-DbBPNIwf.cjs');
5
- var agentGuidance = require('./agentGuidance-6yh_wPwD.cjs');
3
+ var persistence = require('./limits-CfzVummd.cjs');
4
+ var api = require('./types-CRxf_3n8.cjs');
5
+ var agentGuidance = require('./agentGuidance-4HV-BOge.cjs');
6
6
  require('node:fs/promises');
7
7
  require('node:fs');
8
8
  require('node:os');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var persistence = require('./limits-DRrdJ7gr.cjs');
4
+ var persistence = require('./limits-CfzVummd.cjs');
5
5
  var node_events = require('node:events');
6
6
  var socket_ioClient = require('socket.io-client');
7
7
  var z = require('zod');
@@ -1369,7 +1369,7 @@ class RpcHandlerManager {
1369
1369
  }
1370
1370
  }
1371
1371
 
1372
- const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-DbBPNIwf.cjs', document.baseURI).href))));
1372
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CRxf_3n8.cjs', document.baseURI).href))));
1373
1373
  function projectPath() {
1374
1374
  const path$1 = path.resolve(__dirname$1, "..");
1375
1375
  return path$1;
@@ -3050,10 +3050,14 @@ class ApiSessionClient extends node_events.EventEmitter {
3050
3050
  return;
3051
3051
  }
3052
3052
  const localId = data.body.message.localId;
3053
- if (localId && this.directInboundLocalIds.delete(localId)) {
3053
+ const relayAccepted = !!localId && this.directInboundLocalIds.delete(localId);
3054
+ if (localId && this.routedInboundLocalIds.has(localId)) {
3054
3055
  this.lastSeq = messageSeq;
3055
3056
  return;
3056
3057
  }
3058
+ if (relayAccepted) {
3059
+ persistence.logger.debug(`[API] Routing relay-accepted message ${localId} from the central echo (own persist still pending or failed)`);
3060
+ }
3057
3061
  const body = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(data.body.message.content.c));
3058
3062
  persistence.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
3059
3063
  this.routeIncomingMessage(body, localId);
@@ -3306,6 +3310,13 @@ class ApiSessionClient extends node_events.EventEmitter {
3306
3310
  }
3307
3311
  return;
3308
3312
  }
3313
+ const role = typeof message === "object" && message !== null ? message.role : void 0;
3314
+ if (role === "user") {
3315
+ persistence.logger.debug("[API] Inbound user-role message did not match UserMessageSchema; forwarding as generic message", {
3316
+ sourceLocalId: sourceLocalId ?? null,
3317
+ issues: userResult.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`)
3318
+ });
3319
+ }
3309
3320
  this.emit("message", message);
3310
3321
  }
3311
3322
  async fetchMessages() {
@@ -3334,12 +3345,13 @@ class ApiSessionClient extends node_events.EventEmitter {
3334
3345
  maxSeq = message.seq;
3335
3346
  }
3336
3347
  if (skipRouting) continue;
3337
- if (message.localId && this.directInboundLocalIds.delete(message.localId)) {
3338
- continue;
3339
- }
3348
+ const relayAccepted = !!message.localId && this.directInboundLocalIds.delete(message.localId);
3340
3349
  if (message.localId && this.routedInboundLocalIds.has(message.localId)) {
3341
3350
  continue;
3342
3351
  }
3352
+ if (relayAccepted) {
3353
+ persistence.logger.debug(`[API] Routing relay-accepted message ${message.localId} from catch-up fetch (own persist still pending or failed)`);
3354
+ }
3343
3355
  if (message.content?.t !== "encrypted") {
3344
3356
  continue;
3345
3357
  }
@@ -3471,6 +3483,8 @@ class ApiSessionClient extends node_events.EventEmitter {
3471
3483
  }
3472
3484
  callback({ ok: true, messages: stored });
3473
3485
  } catch (error) {
3486
+ const routed = batch.filter((item) => this.routedInboundLocalIds.has(item.localId)).length;
3487
+ persistence.logger.debug(`[API] Relay-delivered persist failed for ${batch.length} message(s) (${routed} already routed via central echo): ${error instanceof Error ? error.message : error}`);
3474
3488
  for (const item of batch) this.directInboundLocalIds.delete(item.localId);
3475
3489
  callback({ ok: false, error: error instanceof Error ? error.message : "Failed to persist session messages" });
3476
3490
  }
@@ -12966,6 +12980,9 @@ class CodexAppServerClient {
12966
12980
  // Tracks in-flight interruptTurn() RPCs so sendTurnAndWait can wait for them
12967
12981
  // before starting a new turn (prevents stale turn/interrupt from aborting the next turn).
12968
12982
  pendingInterrupt = null;
12983
+ // In-flight idle release (B-461); ensureConnected() waits for it so a message
12984
+ // arriving mid-release cannot spawn a second app-server against a dying one.
12985
+ idleRelease = null;
12969
12986
  notificationProtocol = "unknown";
12970
12987
  completedTurnIds = /* @__PURE__ */ new Set();
12971
12988
  rawFileChangesByItemId = /* @__PURE__ */ new Map();
@@ -13299,6 +13316,53 @@ class CodexAppServerClient {
13299
13316
  async disconnect() {
13300
13317
  await this.disconnectInternal();
13301
13318
  }
13319
+ /** True while an app-server child process is owned by this client. */
13320
+ get hasProcess() {
13321
+ return this.process !== null;
13322
+ }
13323
+ /**
13324
+ * Drop the app-server process while the session is idle, keeping the thread
13325
+ * id and defaults so the next turn can `thread/resume` it (B-461). Refuses —
13326
+ * returning false — whenever anything is still in flight: an active turn, a
13327
+ * pending interrupt, or any outstanding RPC. See codexIdleRelease.ts.
13328
+ */
13329
+ async releaseIdleProcess() {
13330
+ if (this.idleRelease) return this.idleRelease;
13331
+ if (!this.connected && !this.process) return false;
13332
+ if (this.pendingTurnCompletion || this.pendingInterrupt || this.pending.size > 0) {
13333
+ persistence.logger.debug("[CodexAppServer] Idle release skipped: work still in flight");
13334
+ return false;
13335
+ }
13336
+ const run = (async () => {
13337
+ const threadId = this._threadId;
13338
+ persistence.logger.debug(`[CodexAppServer] Releasing idle app-server; thread=${threadId ?? "none"} kept for resume`);
13339
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
13340
+ return true;
13341
+ })();
13342
+ this.idleRelease = run;
13343
+ try {
13344
+ return await run;
13345
+ } finally {
13346
+ if (this.idleRelease === run) this.idleRelease = null;
13347
+ }
13348
+ }
13349
+ /**
13350
+ * Make sure an app-server is running and, if a thread is remembered, resumed
13351
+ * in it. Cheap no-op while connected. Covers both the idle release above and
13352
+ * an app-server that exited on its own between turns.
13353
+ */
13354
+ async ensureConnected() {
13355
+ if (this.idleRelease) await this.idleRelease;
13356
+ if (this.connected) return;
13357
+ const threadId = this._threadId;
13358
+ if (this.process) {
13359
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
13360
+ }
13361
+ await this.connect();
13362
+ if (threadId) {
13363
+ await this.resumeThread({ threadId });
13364
+ }
13365
+ }
13302
13366
  buildThreadConfig(mcpServers) {
13303
13367
  return mcpServers ? { mcp_servers: mcpServers } : null;
13304
13368
  }
@@ -13313,6 +13377,7 @@ class CodexAppServerClient {
13313
13377
  }
13314
13378
  // ─── Thread management ──────────────────────────────────────
13315
13379
  async startThread(opts) {
13380
+ await this.ensureConnected();
13316
13381
  const params = {
13317
13382
  model: opts.model ?? null,
13318
13383
  modelProvider: null,
@@ -13340,6 +13405,7 @@ class CodexAppServerClient {
13340
13405
  if (!threadId) {
13341
13406
  throw new Error("No thread available to resume.");
13342
13407
  }
13408
+ await this.ensureConnected();
13343
13409
  const defaults = this.threadDefaults ?? {};
13344
13410
  const params = {
13345
13411
  threadId,
@@ -13367,6 +13433,7 @@ class CodexAppServerClient {
13367
13433
  return { threadId: result.thread.id, model: result.model };
13368
13434
  }
13369
13435
  async forkThread(opts) {
13436
+ await this.ensureConnected();
13370
13437
  const defaults = this.threadDefaults ?? {};
13371
13438
  const params = {
13372
13439
  threadId: opts.threadId,
@@ -13395,6 +13462,7 @@ class CodexAppServerClient {
13395
13462
  return { threadId: result.thread.id, model: result.model, thread: result.thread };
13396
13463
  }
13397
13464
  async readThread(opts) {
13465
+ await this.ensureConnected();
13398
13466
  const params = {
13399
13467
  threadId: opts.threadId,
13400
13468
  includeTurns: opts.includeTurns ?? true
@@ -13402,6 +13470,7 @@ class CodexAppServerClient {
13402
13470
  return await this.request("thread/read", params);
13403
13471
  }
13404
13472
  async rollbackThread(opts) {
13473
+ await this.ensureConnected();
13405
13474
  const params = {
13406
13475
  threadId: opts.threadId,
13407
13476
  numTurns: opts.numTurns
@@ -13409,6 +13478,7 @@ class CodexAppServerClient {
13409
13478
  return await this.request("thread/rollback", params);
13410
13479
  }
13411
13480
  async injectItems(opts) {
13481
+ await this.ensureConnected();
13412
13482
  const params = {
13413
13483
  threadId: opts.threadId,
13414
13484
  items: opts.items
@@ -13513,6 +13583,7 @@ class CodexAppServerClient {
13513
13583
  if (!this._threadId) {
13514
13584
  throw new Error("No active thread. Call startThread first.");
13515
13585
  }
13586
+ await this.ensureConnected();
13516
13587
  const input = [
13517
13588
  { type: "text", text: prompt },
13518
13589
  ...(opts?.images ?? []).map((path) => ({ type: "localImage", path }))
@@ -13558,6 +13629,7 @@ class CodexAppServerClient {
13558
13629
  await this.pendingInterrupt;
13559
13630
  await new Promise((resolve) => setTimeout(resolve, 0));
13560
13631
  }
13632
+ await this.ensureConnected();
13561
13633
  const timeoutMs = opts?.turnTimeoutMs ?? CodexAppServerClient.TURN_TIMEOUT_MS;
13562
13634
  let timer = null;
13563
13635
  const completion = new Promise((resolve) => {
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { l as logger, i as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as decryptBlob, q as prepareClipboardText, g as readSettings, r as readPersistedSessions, s as credentialRelayProblem, p as packageJson, t as readTrackedClaudeSessionIds, u as libsodiumEncryptForPublicKey, v as libsodiumPublicKeyFromSecretKey, x as getRandomBytes, y as redactLogValue } from './limits-kjHZUlSp.mjs';
2
+ import { l as logger, i as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as decryptBlob, q as prepareClipboardText, g as readSettings, r as readPersistedSessions, s as credentialRelayProblem, p as packageJson, t as readTrackedClaudeSessionIds, u as libsodiumEncryptForPublicKey, v as libsodiumPublicKeyFromSecretKey, x as getRandomBytes, y as redactLogValue } from './limits-DEsg_gHi.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { io } from 'socket.io-client';
5
5
  import * as z from 'zod';
@@ -3028,10 +3028,14 @@ class ApiSessionClient extends EventEmitter {
3028
3028
  return;
3029
3029
  }
3030
3030
  const localId = data.body.message.localId;
3031
- if (localId && this.directInboundLocalIds.delete(localId)) {
3031
+ const relayAccepted = !!localId && this.directInboundLocalIds.delete(localId);
3032
+ if (localId && this.routedInboundLocalIds.has(localId)) {
3032
3033
  this.lastSeq = messageSeq;
3033
3034
  return;
3034
3035
  }
3036
+ if (relayAccepted) {
3037
+ logger.debug(`[API] Routing relay-accepted message ${localId} from the central echo (own persist still pending or failed)`);
3038
+ }
3035
3039
  const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
3036
3040
  logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
3037
3041
  this.routeIncomingMessage(body, localId);
@@ -3284,6 +3288,13 @@ class ApiSessionClient extends EventEmitter {
3284
3288
  }
3285
3289
  return;
3286
3290
  }
3291
+ const role = typeof message === "object" && message !== null ? message.role : void 0;
3292
+ if (role === "user") {
3293
+ logger.debug("[API] Inbound user-role message did not match UserMessageSchema; forwarding as generic message", {
3294
+ sourceLocalId: sourceLocalId ?? null,
3295
+ issues: userResult.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`)
3296
+ });
3297
+ }
3287
3298
  this.emit("message", message);
3288
3299
  }
3289
3300
  async fetchMessages() {
@@ -3312,12 +3323,13 @@ class ApiSessionClient extends EventEmitter {
3312
3323
  maxSeq = message.seq;
3313
3324
  }
3314
3325
  if (skipRouting) continue;
3315
- if (message.localId && this.directInboundLocalIds.delete(message.localId)) {
3316
- continue;
3317
- }
3326
+ const relayAccepted = !!message.localId && this.directInboundLocalIds.delete(message.localId);
3318
3327
  if (message.localId && this.routedInboundLocalIds.has(message.localId)) {
3319
3328
  continue;
3320
3329
  }
3330
+ if (relayAccepted) {
3331
+ logger.debug(`[API] Routing relay-accepted message ${message.localId} from catch-up fetch (own persist still pending or failed)`);
3332
+ }
3321
3333
  if (message.content?.t !== "encrypted") {
3322
3334
  continue;
3323
3335
  }
@@ -3449,6 +3461,8 @@ class ApiSessionClient extends EventEmitter {
3449
3461
  }
3450
3462
  callback({ ok: true, messages: stored });
3451
3463
  } catch (error) {
3464
+ const routed = batch.filter((item) => this.routedInboundLocalIds.has(item.localId)).length;
3465
+ logger.debug(`[API] Relay-delivered persist failed for ${batch.length} message(s) (${routed} already routed via central echo): ${error instanceof Error ? error.message : error}`);
3452
3466
  for (const item of batch) this.directInboundLocalIds.delete(item.localId);
3453
3467
  callback({ ok: false, error: error instanceof Error ? error.message : "Failed to persist session messages" });
3454
3468
  }
@@ -12944,6 +12958,9 @@ class CodexAppServerClient {
12944
12958
  // Tracks in-flight interruptTurn() RPCs so sendTurnAndWait can wait for them
12945
12959
  // before starting a new turn (prevents stale turn/interrupt from aborting the next turn).
12946
12960
  pendingInterrupt = null;
12961
+ // In-flight idle release (B-461); ensureConnected() waits for it so a message
12962
+ // arriving mid-release cannot spawn a second app-server against a dying one.
12963
+ idleRelease = null;
12947
12964
  notificationProtocol = "unknown";
12948
12965
  completedTurnIds = /* @__PURE__ */ new Set();
12949
12966
  rawFileChangesByItemId = /* @__PURE__ */ new Map();
@@ -13277,6 +13294,53 @@ class CodexAppServerClient {
13277
13294
  async disconnect() {
13278
13295
  await this.disconnectInternal();
13279
13296
  }
13297
+ /** True while an app-server child process is owned by this client. */
13298
+ get hasProcess() {
13299
+ return this.process !== null;
13300
+ }
13301
+ /**
13302
+ * Drop the app-server process while the session is idle, keeping the thread
13303
+ * id and defaults so the next turn can `thread/resume` it (B-461). Refuses —
13304
+ * returning false — whenever anything is still in flight: an active turn, a
13305
+ * pending interrupt, or any outstanding RPC. See codexIdleRelease.ts.
13306
+ */
13307
+ async releaseIdleProcess() {
13308
+ if (this.idleRelease) return this.idleRelease;
13309
+ if (!this.connected && !this.process) return false;
13310
+ if (this.pendingTurnCompletion || this.pendingInterrupt || this.pending.size > 0) {
13311
+ logger.debug("[CodexAppServer] Idle release skipped: work still in flight");
13312
+ return false;
13313
+ }
13314
+ const run = (async () => {
13315
+ const threadId = this._threadId;
13316
+ logger.debug(`[CodexAppServer] Releasing idle app-server; thread=${threadId ?? "none"} kept for resume`);
13317
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
13318
+ return true;
13319
+ })();
13320
+ this.idleRelease = run;
13321
+ try {
13322
+ return await run;
13323
+ } finally {
13324
+ if (this.idleRelease === run) this.idleRelease = null;
13325
+ }
13326
+ }
13327
+ /**
13328
+ * Make sure an app-server is running and, if a thread is remembered, resumed
13329
+ * in it. Cheap no-op while connected. Covers both the idle release above and
13330
+ * an app-server that exited on its own between turns.
13331
+ */
13332
+ async ensureConnected() {
13333
+ if (this.idleRelease) await this.idleRelease;
13334
+ if (this.connected) return;
13335
+ const threadId = this._threadId;
13336
+ if (this.process) {
13337
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
13338
+ }
13339
+ await this.connect();
13340
+ if (threadId) {
13341
+ await this.resumeThread({ threadId });
13342
+ }
13343
+ }
13280
13344
  buildThreadConfig(mcpServers) {
13281
13345
  return mcpServers ? { mcp_servers: mcpServers } : null;
13282
13346
  }
@@ -13291,6 +13355,7 @@ class CodexAppServerClient {
13291
13355
  }
13292
13356
  // ─── Thread management ──────────────────────────────────────
13293
13357
  async startThread(opts) {
13358
+ await this.ensureConnected();
13294
13359
  const params = {
13295
13360
  model: opts.model ?? null,
13296
13361
  modelProvider: null,
@@ -13318,6 +13383,7 @@ class CodexAppServerClient {
13318
13383
  if (!threadId) {
13319
13384
  throw new Error("No thread available to resume.");
13320
13385
  }
13386
+ await this.ensureConnected();
13321
13387
  const defaults = this.threadDefaults ?? {};
13322
13388
  const params = {
13323
13389
  threadId,
@@ -13345,6 +13411,7 @@ class CodexAppServerClient {
13345
13411
  return { threadId: result.thread.id, model: result.model };
13346
13412
  }
13347
13413
  async forkThread(opts) {
13414
+ await this.ensureConnected();
13348
13415
  const defaults = this.threadDefaults ?? {};
13349
13416
  const params = {
13350
13417
  threadId: opts.threadId,
@@ -13373,6 +13440,7 @@ class CodexAppServerClient {
13373
13440
  return { threadId: result.thread.id, model: result.model, thread: result.thread };
13374
13441
  }
13375
13442
  async readThread(opts) {
13443
+ await this.ensureConnected();
13376
13444
  const params = {
13377
13445
  threadId: opts.threadId,
13378
13446
  includeTurns: opts.includeTurns ?? true
@@ -13380,6 +13448,7 @@ class CodexAppServerClient {
13380
13448
  return await this.request("thread/read", params);
13381
13449
  }
13382
13450
  async rollbackThread(opts) {
13451
+ await this.ensureConnected();
13383
13452
  const params = {
13384
13453
  threadId: opts.threadId,
13385
13454
  numTurns: opts.numTurns
@@ -13387,6 +13456,7 @@ class CodexAppServerClient {
13387
13456
  return await this.request("thread/rollback", params);
13388
13457
  }
13389
13458
  async injectItems(opts) {
13459
+ await this.ensureConnected();
13390
13460
  const params = {
13391
13461
  threadId: opts.threadId,
13392
13462
  items: opts.items
@@ -13491,6 +13561,7 @@ class CodexAppServerClient {
13491
13561
  if (!this._threadId) {
13492
13562
  throw new Error("No active thread. Call startThread first.");
13493
13563
  }
13564
+ await this.ensureConnected();
13494
13565
  const input = [
13495
13566
  { type: "text", text: prompt },
13496
13567
  ...(opts?.images ?? []).map((path) => ({ type: "localImage", path }))
@@ -13536,6 +13607,7 @@ class CodexAppServerClient {
13536
13607
  await this.pendingInterrupt;
13537
13608
  await new Promise((resolve) => setTimeout(resolve, 0));
13538
13609
  }
13610
+ await this.ensureConnected();
13539
13611
  const timeoutMs = opts?.turnTimeoutMs ?? CodexAppServerClient.TURN_TIMEOUT_MS;
13540
13612
  let timer = null;
13541
13613
  const completion = new Promise((resolve) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.136",
3
+ "version": "0.2.138",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -113,12 +113,12 @@
113
113
  "zod": "^4.0.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "very-happy-tools-arm64-darwin": "0.2.136",
117
- "very-happy-tools-x64-darwin": "0.2.136",
118
- "very-happy-tools-arm64-linux": "0.2.136",
119
- "very-happy-tools-x64-linux": "0.2.136",
120
- "very-happy-tools-arm64-win32": "0.2.136",
121
- "very-happy-tools-x64-win32": "0.2.136"
116
+ "very-happy-tools-arm64-darwin": "0.2.138",
117
+ "very-happy-tools-x64-darwin": "0.2.138",
118
+ "very-happy-tools-arm64-linux": "0.2.138",
119
+ "very-happy-tools-x64-linux": "0.2.138",
120
+ "very-happy-tools-arm64-win32": "0.2.138",
121
+ "very-happy-tools-x64-win32": "0.2.138"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@slopus/happy-wire": "workspace:*",