happy-coder 0.10.0 → 0.10.1

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.
@@ -3,7 +3,7 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var types = require('./types-CQOz_mPp.cjs');
6
+ var types = require('./types-DOKP_I5s.cjs');
7
7
  var node_child_process = require('node:child_process');
8
8
  var node_path = require('node:path');
9
9
  var node_readline = require('node:readline');
@@ -186,9 +186,39 @@ function trimIdent(text) {
186
186
  return trimmedLines.join("\n");
187
187
  }
188
188
 
189
- const systemPrompt = trimIdent(`
190
- ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
189
+ function getClaudeSettingsPath() {
190
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || node_path.join(os.homedir(), ".claude");
191
+ return node_path.join(claudeConfigDir, "settings.json");
192
+ }
193
+ function readClaudeSettings() {
194
+ try {
195
+ const settingsPath = getClaudeSettingsPath();
196
+ if (!fs.existsSync(settingsPath)) {
197
+ types.logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
198
+ return null;
199
+ }
200
+ const settingsContent = fs.readFileSync(settingsPath, "utf-8");
201
+ const settings = JSON.parse(settingsContent);
202
+ types.logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
203
+ types.logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
204
+ return settings;
205
+ } catch (error) {
206
+ types.logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
207
+ return null;
208
+ }
209
+ }
210
+ function shouldIncludeCoAuthoredBy() {
211
+ const settings = readClaudeSettings();
212
+ if (!settings || settings.includeCoAuthoredBy === void 0) {
213
+ return true;
214
+ }
215
+ return settings.includeCoAuthoredBy;
216
+ }
191
217
 
218
+ const BASE_SYSTEM_PROMPT = (() => trimIdent(`
219
+ ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
220
+ `))();
221
+ const CO_AUTHORED_CREDITS = (() => trimIdent(`
192
222
  When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
193
223
 
194
224
  <main commit message>
@@ -198,7 +228,15 @@ const systemPrompt = trimIdent(`
198
228
 
199
229
  Co-Authored-By: Claude <noreply@anthropic.com>
200
230
  Co-Authored-By: Happy <yesreply@happy.engineering>
201
- `);
231
+ `))();
232
+ const systemPrompt = (() => {
233
+ const includeCoAuthored = shouldIncludeCoAuthoredBy();
234
+ if (includeCoAuthored) {
235
+ return BASE_SYSTEM_PROMPT + "\n\n" + CO_AUTHORED_CREDITS;
236
+ } else {
237
+ return BASE_SYSTEM_PROMPT;
238
+ }
239
+ })();
202
240
 
203
241
  const claudeCliPath = node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_local_launcher.cjs"));
204
242
  async function claudeLocal(opts) {
@@ -943,7 +981,7 @@ class AbortError extends Error {
943
981
  }
944
982
  }
945
983
 
946
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-ettJex_e.cjs', document.baseURI).href)));
984
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-B_nemqpL.cjs', document.baseURI).href)));
947
985
  const __dirname$1 = node_path.join(__filename$1, "..");
948
986
  function getDefaultClaudeCodePath() {
949
987
  return node_path.join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
@@ -5765,7 +5803,7 @@ async function handleConnectVendor(vendor, displayName) {
5765
5803
  return;
5766
5804
  } else if (subcommand === "codex") {
5767
5805
  try {
5768
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-QdQBx9HK.cjs'); });
5806
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-BjT2Vmcd.cjs'); });
5769
5807
  let startedBy = void 0;
5770
5808
  for (let i = 1; i < args.length; i++) {
5771
5809
  if (args[i] === "--started-by") {
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import os$1, { homedir } from 'node:os';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
- import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, f as AsyncLock, g as readDaemonState, h as clearDaemonState, b as packageJson, c as configuration, r as readSettings, i as readCredentials, j as encodeBase64, u as updateSettings, k as encodeBase64Url, m as decodeBase64, w as writeCredentialsLegacy, n as writeCredentialsDataKey, o as acquireDaemonLock, q as writeDaemonState, A as ApiClient, s as releaseDaemonLock, t as clearCredentials, v as clearMachineId, x as getLatestDaemonLog } from './types-8Ad05p3x.mjs';
4
+ import { l as logger, p as projectPath, d as backoff, e as delay, R as RawJSONLinesSchema, f as AsyncLock, g as readDaemonState, h as clearDaemonState, b as packageJson, c as configuration, r as readSettings, i as readCredentials, j as encodeBase64, u as updateSettings, k as encodeBase64Url, m as decodeBase64, w as writeCredentialsLegacy, n as writeCredentialsDataKey, o as acquireDaemonLock, q as writeDaemonState, A as ApiClient, s as releaseDaemonLock, t as clearCredentials, v as clearMachineId, x as getLatestDaemonLog } from './types-CYZG1S69.mjs';
5
5
  import { spawn, execSync, execFileSync } from 'node:child_process';
6
6
  import { resolve, join } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
@@ -164,9 +164,39 @@ function trimIdent(text) {
164
164
  return trimmedLines.join("\n");
165
165
  }
166
166
 
167
- const systemPrompt = trimIdent(`
168
- ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
167
+ function getClaudeSettingsPath() {
168
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
169
+ return join(claudeConfigDir, "settings.json");
170
+ }
171
+ function readClaudeSettings() {
172
+ try {
173
+ const settingsPath = getClaudeSettingsPath();
174
+ if (!existsSync(settingsPath)) {
175
+ logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
176
+ return null;
177
+ }
178
+ const settingsContent = readFileSync(settingsPath, "utf-8");
179
+ const settings = JSON.parse(settingsContent);
180
+ logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
181
+ logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
182
+ return settings;
183
+ } catch (error) {
184
+ logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
185
+ return null;
186
+ }
187
+ }
188
+ function shouldIncludeCoAuthoredBy() {
189
+ const settings = readClaudeSettings();
190
+ if (!settings || settings.includeCoAuthoredBy === void 0) {
191
+ return true;
192
+ }
193
+ return settings.includeCoAuthoredBy;
194
+ }
169
195
 
196
+ const BASE_SYSTEM_PROMPT = (() => trimIdent(`
197
+ ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
198
+ `))();
199
+ const CO_AUTHORED_CREDITS = (() => trimIdent(`
170
200
  When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
171
201
 
172
202
  <main commit message>
@@ -176,7 +206,15 @@ const systemPrompt = trimIdent(`
176
206
 
177
207
  Co-Authored-By: Claude <noreply@anthropic.com>
178
208
  Co-Authored-By: Happy <yesreply@happy.engineering>
179
- `);
209
+ `))();
210
+ const systemPrompt = (() => {
211
+ const includeCoAuthored = shouldIncludeCoAuthoredBy();
212
+ if (includeCoAuthored) {
213
+ return BASE_SYSTEM_PROMPT + "\n\n" + CO_AUTHORED_CREDITS;
214
+ } else {
215
+ return BASE_SYSTEM_PROMPT;
216
+ }
217
+ })();
180
218
 
181
219
  const claudeCliPath = resolve(join(projectPath(), "scripts", "claude_local_launcher.cjs"));
182
220
  async function claudeLocal(opts) {
@@ -5743,7 +5781,7 @@ async function handleConnectVendor(vendor, displayName) {
5743
5781
  return;
5744
5782
  } else if (subcommand === "codex") {
5745
5783
  try {
5746
- const { runCodex } = await import('./runCodex-HlLNepHI.mjs');
5784
+ const { runCodex } = await import('./runCodex-CHAghw9e.mjs');
5747
5785
  let startedBy = void 0;
5748
5786
  for (let i = 1; i < args.length; i++) {
5749
5787
  if (args[i] === "--started-by") {
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  require('chalk');
4
- require('./index-ettJex_e.cjs');
5
- require('./types-CQOz_mPp.cjs');
4
+ require('./index-B_nemqpL.cjs');
5
+ require('./types-DOKP_I5s.cjs');
6
6
  require('zod');
7
7
  require('node:child_process');
8
8
  require('node:os');
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'chalk';
2
- import './index-BI37NnoW.mjs';
3
- import './types-8Ad05p3x.mjs';
2
+ import './index-Cp1f1oto.mjs';
3
+ import './types-CYZG1S69.mjs';
4
4
  import 'zod';
5
5
  import 'node:child_process';
6
6
  import 'node:os';
package/dist/lib.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types-CQOz_mPp.cjs');
3
+ var types = require('./types-DOKP_I5s.cjs');
4
4
  require('axios');
5
5
  require('chalk');
6
6
  require('fs');
package/dist/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-8Ad05p3x.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-CYZG1S69.mjs';
2
2
  import 'axios';
3
3
  import 'chalk';
4
4
  import 'fs';
@@ -2,13 +2,13 @@
2
2
 
3
3
  var ink = require('ink');
4
4
  var React = require('react');
5
- var types = require('./types-CQOz_mPp.cjs');
5
+ var types = require('./types-DOKP_I5s.cjs');
6
6
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
7
7
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
8
  var z = require('zod');
9
9
  var types_js = require('@modelcontextprotocol/sdk/types.js');
10
10
  var node_crypto = require('node:crypto');
11
- var index = require('./index-ettJex_e.cjs');
11
+ var index = require('./index-B_nemqpL.cjs');
12
12
  var os = require('node:os');
13
13
  var node_path = require('node:path');
14
14
  var fs = require('node:fs');
@@ -183,8 +183,16 @@ class CodexMcpClient {
183
183
  return this.sessionId !== null;
184
184
  }
185
185
  clearSession() {
186
+ const previousSessionId = this.sessionId;
186
187
  this.sessionId = null;
187
- types.logger.debug("[CodexMCP] Session cleared");
188
+ types.logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
189
+ }
190
+ /**
191
+ * Store the current session ID without clearing it, useful for abort handling
192
+ */
193
+ storeSessionForResume() {
194
+ types.logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
195
+ return this.sessionId;
188
196
  }
189
197
  async disconnect() {
190
198
  if (!this.connected) return;
@@ -781,25 +789,30 @@ async function runCodex(opts) {
781
789
  }
782
790
  let abortController = new AbortController();
783
791
  let shouldExit = false;
792
+ let storedSessionIdForResume = null;
784
793
  async function handleAbort() {
785
- types.logger.debug("[Codex] Abort requested");
794
+ types.logger.debug("[Codex] Abort requested - stopping current task");
786
795
  try {
796
+ if (client.hasActiveSession()) {
797
+ storedSessionIdForResume = client.storeSessionForResume();
798
+ types.logger.debug("[Codex] Stored session for resume:", storedSessionIdForResume);
799
+ }
787
800
  abortController.abort();
788
801
  messageQueue.reset();
789
802
  permissionHandler.reset();
790
803
  reasoningProcessor.abort();
791
804
  diffProcessor.reset();
792
- types.logger.debug("[Codex] Abort completed");
805
+ types.logger.debug("[Codex] Abort completed - session remains active");
793
806
  } catch (error) {
794
807
  types.logger.debug("[Codex] Error during abort:", error);
795
808
  } finally {
796
809
  abortController = new AbortController();
797
810
  }
798
811
  }
799
- const cleanup = async () => {
800
- types.logger.debug("[Codex] Cleanup start");
812
+ const handleKillSession = async () => {
813
+ types.logger.debug("[Codex] Kill session requested - terminating process");
801
814
  await handleAbort();
802
- types.logger.debug("[Codex] Cleanup completed");
815
+ types.logger.debug("[Codex] Abort completed, proceeding with termination");
803
816
  try {
804
817
  if (session) {
805
818
  session.updateMetadata((currentMetadata) => ({
@@ -815,15 +828,15 @@ async function runCodex(opts) {
815
828
  }
816
829
  index.stopCaffeinate();
817
830
  happyServer.stop();
818
- types.logger.debug("[Codex] Cleanup complete, exiting");
831
+ types.logger.debug("[Codex] Session termination complete, exiting");
819
832
  process.exit(0);
820
833
  } catch (error) {
821
- types.logger.debug("[Codex] Error during cleanup:", error);
834
+ types.logger.debug("[Codex] Error during session termination:", error);
822
835
  process.exit(1);
823
836
  }
824
837
  };
825
838
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
826
- index.registerKillSessionHandler(session.rpcHandlerManager, cleanup);
839
+ index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
827
840
  const messageBuffer = new index.MessageBuffer();
828
841
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
829
842
  let inkInstance = null;
@@ -1039,8 +1052,13 @@ async function runCodex(opts) {
1039
1052
  let message = pending;
1040
1053
  pending = null;
1041
1054
  if (!message) {
1042
- const batch = await messageQueue.waitForMessagesAndGetAsString(abortController.signal);
1043
- if (!batch || shouldExit) {
1055
+ const waitSignal = abortController.signal;
1056
+ const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
1057
+ if (!batch) {
1058
+ if (waitSignal.aborted && !shouldExit) {
1059
+ types.logger.debug("[codex]: Wait aborted while idle; ignoring and continuing");
1060
+ continue;
1061
+ }
1044
1062
  types.logger.debug(`[codex]: batch=${!!batch}, shouldExit=${shouldExit}`);
1045
1063
  break;
1046
1064
  }
@@ -1113,9 +1131,22 @@ async function runCodex(opts) {
1113
1131
  if (message.mode.model) {
1114
1132
  startConfig.model = message.mode.model;
1115
1133
  }
1134
+ let resumeFile = null;
1116
1135
  if (nextExperimentalResume) {
1117
- startConfig.config.experimental_resume = nextExperimentalResume;
1136
+ resumeFile = nextExperimentalResume;
1118
1137
  nextExperimentalResume = null;
1138
+ types.logger.debug("[Codex] Using resume file from mode change:", resumeFile);
1139
+ } else if (storedSessionIdForResume) {
1140
+ const abortResumeFile = findCodexResumeFile(storedSessionIdForResume);
1141
+ if (abortResumeFile) {
1142
+ resumeFile = abortResumeFile;
1143
+ types.logger.debug("[Codex] Using resume file from aborted session:", resumeFile);
1144
+ messageBuffer.addMessage("Resuming from aborted session...", "status");
1145
+ }
1146
+ storedSessionIdForResume = null;
1147
+ }
1148
+ if (resumeFile) {
1149
+ startConfig.config.experimental_resume = resumeFile;
1119
1150
  }
1120
1151
  await client.startSession(
1121
1152
  startConfig,
@@ -1131,12 +1162,20 @@ async function runCodex(opts) {
1131
1162
  }
1132
1163
  } catch (error) {
1133
1164
  types.logger.warn("Error in codex session:", error);
1134
- if (error instanceof Error && error.name === "AbortError") {
1165
+ const isAbortError = error instanceof Error && error.name === "AbortError";
1166
+ if (isAbortError) {
1135
1167
  messageBuffer.addMessage("Aborted by user", "status");
1136
1168
  session.sendSessionEvent({ type: "message", message: "Aborted by user" });
1169
+ wasCreated = false;
1170
+ currentModeHash = null;
1171
+ types.logger.debug("[Codex] Marked session as not created after abort for proper resume");
1137
1172
  } else {
1138
1173
  messageBuffer.addMessage("Process exited unexpectedly", "status");
1139
1174
  session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
1175
+ if (client.hasActiveSession()) {
1176
+ storedSessionIdForResume = client.storeSessionForResume();
1177
+ types.logger.debug("[Codex] Stored session after unexpected error:", storedSessionIdForResume);
1178
+ }
1140
1179
  }
1141
1180
  } finally {
1142
1181
  permissionHandler.reset();
@@ -1148,7 +1187,7 @@ async function runCodex(opts) {
1148
1187
  }
1149
1188
  }
1150
1189
  } finally {
1151
- types.logger.debug("[codex]: Cleanup start");
1190
+ types.logger.debug("[codex]: Final cleanup start");
1152
1191
  logActiveHandles("cleanup-start");
1153
1192
  try {
1154
1193
  types.logger.debug("[codex]: sendSessionDeath");
@@ -1189,7 +1228,7 @@ async function runCodex(opts) {
1189
1228
  }
1190
1229
  messageBuffer.clear();
1191
1230
  logActiveHandles("cleanup-end");
1192
- types.logger.debug("[codex]: Cleanup completed");
1231
+ types.logger.debug("[codex]: Final cleanup completed");
1193
1232
  }
1194
1233
  }
1195
1234
 
@@ -1,12 +1,12 @@
1
1
  import { useStdout, useInput, Box, Text, render } from 'ink';
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
- import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-8Ad05p3x.mjs';
3
+ import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-CYZG1S69.mjs';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
6
  import { z } from 'zod';
7
7
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { randomUUID } from 'node:crypto';
9
- import { i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, h as hashObject, r as registerKillSessionHandler, a as MessageBuffer, s as startHappyServer, t as trimIdent, b as stopCaffeinate } from './index-BI37NnoW.mjs';
9
+ import { i as initialMachineMetadata, n as notifyDaemonSessionStarted, M as MessageQueue2, h as hashObject, r as registerKillSessionHandler, a as MessageBuffer, s as startHappyServer, t as trimIdent, b as stopCaffeinate } from './index-Cp1f1oto.mjs';
10
10
  import os from 'node:os';
11
11
  import { resolve, join } from 'node:path';
12
12
  import fs from 'node:fs';
@@ -181,8 +181,16 @@ class CodexMcpClient {
181
181
  return this.sessionId !== null;
182
182
  }
183
183
  clearSession() {
184
+ const previousSessionId = this.sessionId;
184
185
  this.sessionId = null;
185
- logger.debug("[CodexMCP] Session cleared");
186
+ logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
187
+ }
188
+ /**
189
+ * Store the current session ID without clearing it, useful for abort handling
190
+ */
191
+ storeSessionForResume() {
192
+ logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
193
+ return this.sessionId;
186
194
  }
187
195
  async disconnect() {
188
196
  if (!this.connected) return;
@@ -779,25 +787,30 @@ async function runCodex(opts) {
779
787
  }
780
788
  let abortController = new AbortController();
781
789
  let shouldExit = false;
790
+ let storedSessionIdForResume = null;
782
791
  async function handleAbort() {
783
- logger.debug("[Codex] Abort requested");
792
+ logger.debug("[Codex] Abort requested - stopping current task");
784
793
  try {
794
+ if (client.hasActiveSession()) {
795
+ storedSessionIdForResume = client.storeSessionForResume();
796
+ logger.debug("[Codex] Stored session for resume:", storedSessionIdForResume);
797
+ }
785
798
  abortController.abort();
786
799
  messageQueue.reset();
787
800
  permissionHandler.reset();
788
801
  reasoningProcessor.abort();
789
802
  diffProcessor.reset();
790
- logger.debug("[Codex] Abort completed");
803
+ logger.debug("[Codex] Abort completed - session remains active");
791
804
  } catch (error) {
792
805
  logger.debug("[Codex] Error during abort:", error);
793
806
  } finally {
794
807
  abortController = new AbortController();
795
808
  }
796
809
  }
797
- const cleanup = async () => {
798
- logger.debug("[Codex] Cleanup start");
810
+ const handleKillSession = async () => {
811
+ logger.debug("[Codex] Kill session requested - terminating process");
799
812
  await handleAbort();
800
- logger.debug("[Codex] Cleanup completed");
813
+ logger.debug("[Codex] Abort completed, proceeding with termination");
801
814
  try {
802
815
  if (session) {
803
816
  session.updateMetadata((currentMetadata) => ({
@@ -813,15 +826,15 @@ async function runCodex(opts) {
813
826
  }
814
827
  stopCaffeinate();
815
828
  happyServer.stop();
816
- logger.debug("[Codex] Cleanup complete, exiting");
829
+ logger.debug("[Codex] Session termination complete, exiting");
817
830
  process.exit(0);
818
831
  } catch (error) {
819
- logger.debug("[Codex] Error during cleanup:", error);
832
+ logger.debug("[Codex] Error during session termination:", error);
820
833
  process.exit(1);
821
834
  }
822
835
  };
823
836
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
824
- registerKillSessionHandler(session.rpcHandlerManager, cleanup);
837
+ registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
825
838
  const messageBuffer = new MessageBuffer();
826
839
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
827
840
  let inkInstance = null;
@@ -1037,8 +1050,13 @@ async function runCodex(opts) {
1037
1050
  let message = pending;
1038
1051
  pending = null;
1039
1052
  if (!message) {
1040
- const batch = await messageQueue.waitForMessagesAndGetAsString(abortController.signal);
1041
- if (!batch || shouldExit) {
1053
+ const waitSignal = abortController.signal;
1054
+ const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
1055
+ if (!batch) {
1056
+ if (waitSignal.aborted && !shouldExit) {
1057
+ logger.debug("[codex]: Wait aborted while idle; ignoring and continuing");
1058
+ continue;
1059
+ }
1042
1060
  logger.debug(`[codex]: batch=${!!batch}, shouldExit=${shouldExit}`);
1043
1061
  break;
1044
1062
  }
@@ -1111,9 +1129,22 @@ async function runCodex(opts) {
1111
1129
  if (message.mode.model) {
1112
1130
  startConfig.model = message.mode.model;
1113
1131
  }
1132
+ let resumeFile = null;
1114
1133
  if (nextExperimentalResume) {
1115
- startConfig.config.experimental_resume = nextExperimentalResume;
1134
+ resumeFile = nextExperimentalResume;
1116
1135
  nextExperimentalResume = null;
1136
+ logger.debug("[Codex] Using resume file from mode change:", resumeFile);
1137
+ } else if (storedSessionIdForResume) {
1138
+ const abortResumeFile = findCodexResumeFile(storedSessionIdForResume);
1139
+ if (abortResumeFile) {
1140
+ resumeFile = abortResumeFile;
1141
+ logger.debug("[Codex] Using resume file from aborted session:", resumeFile);
1142
+ messageBuffer.addMessage("Resuming from aborted session...", "status");
1143
+ }
1144
+ storedSessionIdForResume = null;
1145
+ }
1146
+ if (resumeFile) {
1147
+ startConfig.config.experimental_resume = resumeFile;
1117
1148
  }
1118
1149
  await client.startSession(
1119
1150
  startConfig,
@@ -1129,12 +1160,20 @@ async function runCodex(opts) {
1129
1160
  }
1130
1161
  } catch (error) {
1131
1162
  logger.warn("Error in codex session:", error);
1132
- if (error instanceof Error && error.name === "AbortError") {
1163
+ const isAbortError = error instanceof Error && error.name === "AbortError";
1164
+ if (isAbortError) {
1133
1165
  messageBuffer.addMessage("Aborted by user", "status");
1134
1166
  session.sendSessionEvent({ type: "message", message: "Aborted by user" });
1167
+ wasCreated = false;
1168
+ currentModeHash = null;
1169
+ logger.debug("[Codex] Marked session as not created after abort for proper resume");
1135
1170
  } else {
1136
1171
  messageBuffer.addMessage("Process exited unexpectedly", "status");
1137
1172
  session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
1173
+ if (client.hasActiveSession()) {
1174
+ storedSessionIdForResume = client.storeSessionForResume();
1175
+ logger.debug("[Codex] Stored session after unexpected error:", storedSessionIdForResume);
1176
+ }
1138
1177
  }
1139
1178
  } finally {
1140
1179
  permissionHandler.reset();
@@ -1146,7 +1185,7 @@ async function runCodex(opts) {
1146
1185
  }
1147
1186
  }
1148
1187
  } finally {
1149
- logger.debug("[codex]: Cleanup start");
1188
+ logger.debug("[codex]: Final cleanup start");
1150
1189
  logActiveHandles("cleanup-start");
1151
1190
  try {
1152
1191
  logger.debug("[codex]: sendSessionDeath");
@@ -1187,7 +1226,7 @@ async function runCodex(opts) {
1187
1226
  }
1188
1227
  messageBuffer.clear();
1189
1228
  logActiveHandles("cleanup-end");
1190
- logger.debug("[codex]: Cleanup completed");
1229
+ logger.debug("[codex]: Final cleanup completed");
1191
1230
  }
1192
1231
  }
1193
1232
 
@@ -21,7 +21,7 @@ import { platform } from 'os';
21
21
  import { Expo } from 'expo-server-sdk';
22
22
 
23
23
  var name = "happy-coder";
24
- var version = "0.10.0";
24
+ var version = "0.10.1";
25
25
  var description = "Mobile and Web client for Claude Code and Codex";
26
26
  var author = "Kirill Dubovitskiy";
27
27
  var license = "MIT";
@@ -89,8 +89,8 @@ var scripts = {
89
89
  postinstall: "node scripts/unpack-tools.cjs"
90
90
  };
91
91
  var dependencies = {
92
- "@anthropic-ai/claude-code": "^1.0.102",
93
- "@anthropic-ai/sdk": "^0.56.0",
92
+ "@anthropic-ai/claude-code": "1.0.120",
93
+ "@anthropic-ai/sdk": "0.56.0",
94
94
  "@modelcontextprotocol/sdk": "^1.15.1",
95
95
  "@stablelib/base64": "^2.0.1",
96
96
  "@stablelib/hex": "^2.0.1",
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "happy-coder";
45
- var version = "0.10.0";
45
+ var version = "0.10.1";
46
46
  var description = "Mobile and Web client for Claude Code and Codex";
47
47
  var author = "Kirill Dubovitskiy";
48
48
  var license = "MIT";
@@ -110,8 +110,8 @@ var scripts = {
110
110
  postinstall: "node scripts/unpack-tools.cjs"
111
111
  };
112
112
  var dependencies = {
113
- "@anthropic-ai/claude-code": "^1.0.102",
114
- "@anthropic-ai/sdk": "^0.56.0",
113
+ "@anthropic-ai/claude-code": "1.0.120",
114
+ "@anthropic-ai/sdk": "0.56.0",
115
115
  "@modelcontextprotocol/sdk": "^1.15.1",
116
116
  "@stablelib/base64": "^2.0.1",
117
117
  "@stablelib/hex": "^2.0.1",
@@ -1017,7 +1017,7 @@ class RpcHandlerManager {
1017
1017
  }
1018
1018
  }
1019
1019
 
1020
- 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-CQOz_mPp.cjs', document.baseURI).href))));
1020
+ 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-DOKP_I5s.cjs', document.baseURI).href))));
1021
1021
  function projectPath() {
1022
1022
  const path$1 = path.resolve(__dirname$1, "..");
1023
1023
  return path$1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Mobile and Web client for Claude Code and Codex",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -68,8 +68,8 @@
68
68
  "postinstall": "node scripts/unpack-tools.cjs"
69
69
  },
70
70
  "dependencies": {
71
- "@anthropic-ai/claude-code": "^1.0.102",
72
- "@anthropic-ai/sdk": "^0.56.0",
71
+ "@anthropic-ai/claude-code": "1.0.120",
72
+ "@anthropic-ai/sdk": "0.56.0",
73
73
  "@modelcontextprotocol/sdk": "^1.15.1",
74
74
  "@stablelib/base64": "^2.0.1",
75
75
  "@stablelib/hex": "^2.0.1",