happy-coder 0.1.11 → 0.1.12

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.
package/bin/happy CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
+ process.env.NODE_NO_WARNINGS = '1';
2
3
  import '../dist/index.mjs'
package/bin/happy.cmd CHANGED
@@ -1,2 +1,3 @@
1
1
  @echo off
2
+ set NODE_NO_WARNINGS=1
2
3
  node "%~dp0\happy" %*
package/dist/index.cjs CHANGED
@@ -264,7 +264,7 @@ async function claudeRemote(opts) {
264
264
  try {
265
265
  types.logger.debug(`[claudeRemote] Starting to iterate over response`);
266
266
  for await (const message of response) {
267
- types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
267
+ types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
268
268
  formatClaudeMessage(message, opts.onAssistantResult);
269
269
  if (message.type === "system" && message.subtype === "init") {
270
270
  types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
@@ -600,8 +600,8 @@ function createSessionScanner(opts) {
600
600
  let pendingSessions = /* @__PURE__ */ new Set();
601
601
  let currentSessionId = null;
602
602
  let watchers = /* @__PURE__ */ new Map();
603
- let processedMessages = /* @__PURE__ */ new Set();
604
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
603
+ let processedMessageKeys = /* @__PURE__ */ new Set();
604
+ let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
605
605
  const sync = new InvalidateSync(async () => {
606
606
  types.logger.debug(`[SESSION_SCANNER] Syncing...`);
607
607
  let sessions = [];
@@ -633,16 +633,16 @@ function createSessionScanner(opts) {
633
633
  continue;
634
634
  }
635
635
  let key = getMessageKey(parsed.data);
636
- if (processedMessages.has(key)) {
636
+ if (processedMessageKeys.has(key)) {
637
637
  continue;
638
638
  }
639
- processedMessages.add(key);
639
+ processedMessageKeys.add(key);
640
640
  types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
641
641
  types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
642
642
  if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
643
- const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
644
- if (currentCounter && currentCounter > 0) {
645
- seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
643
+ if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
644
+ types.logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
645
+ unmatchedServerMessageContents.delete(parsed.data.message.content);
646
646
  continue;
647
647
  }
648
648
  }
@@ -703,13 +703,18 @@ function createSessionScanner(opts) {
703
703
  sync.invalidate();
704
704
  },
705
705
  onRemoteUserMessageForDeduplication: (messageContent) => {
706
- seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
706
+ types.logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
707
+ unmatchedServerMessageContents.add(messageContent);
707
708
  }
708
709
  };
709
710
  }
710
711
  function getMessageKey(message) {
711
712
  if (message.type === "user") {
712
- return `user:${message.uuid}`;
713
+ if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
714
+ return `user-message-content:${stableStringify(message.message.content[0].text)}`;
715
+ } else {
716
+ return `user-message-content:${stableStringify(message.message.content)}`;
717
+ }
713
718
  } else if (message.type === "assistant") {
714
719
  const { usage, ...messageWithoutUsage } = message.message;
715
720
  return stableStringify(messageWithoutUsage);
@@ -721,6 +726,9 @@ function getMessageKey(message) {
721
726
  return `unknown:<error, this should be unreachable>`;
722
727
  }
723
728
  function stableStringify(obj) {
729
+ if (!obj) {
730
+ return "null";
731
+ }
724
732
  return JSON.stringify(sortKeys(obj), null, 2);
725
733
  }
726
734
  function sortKeys(value) {
@@ -784,6 +792,11 @@ async function loop(opts) {
784
792
  interactiveAbortController.abort();
785
793
  }
786
794
  });
795
+ opts.session.setHandler("abort", () => {
796
+ if (onMessage) {
797
+ onMessage();
798
+ }
799
+ });
787
800
  onMessage = () => {
788
801
  if (!interactiveAbortController.signal.aborted) {
789
802
  abortedOutside = true;
@@ -793,18 +806,32 @@ async function loop(opts) {
793
806
  opts.onModeChange(mode);
794
807
  }
795
808
  }
809
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
796
810
  interactiveAbortController.abort();
797
811
  }
798
812
  onMessage = null;
799
813
  };
800
- await claudeLocal({
801
- path: opts.path,
802
- sessionId,
803
- onSessionFound,
804
- abort: interactiveAbortController.signal,
805
- claudeEnvVars: opts.claudeEnvVars,
806
- claudeArgs: opts.claudeArgs
807
- });
814
+ try {
815
+ if (opts.onProcessStart) {
816
+ opts.onProcessStart("local");
817
+ }
818
+ await claudeLocal({
819
+ path: opts.path,
820
+ sessionId,
821
+ onSessionFound,
822
+ abort: interactiveAbortController.signal,
823
+ claudeEnvVars: opts.claudeEnvVars,
824
+ claudeArgs: opts.claudeArgs
825
+ });
826
+ } catch (e) {
827
+ if (!interactiveAbortController.signal.aborted) {
828
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
829
+ }
830
+ } finally {
831
+ if (opts.onProcessStop) {
832
+ opts.onProcessStop("local");
833
+ }
834
+ }
808
835
  onMessage = null;
809
836
  if (!abortedOutside) {
810
837
  return;
@@ -829,6 +856,7 @@ async function loop(opts) {
829
856
  opts.onModeChange(mode);
830
857
  }
831
858
  }
859
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
832
860
  remoteAbortController.abort();
833
861
  }
834
862
  if (process.stdin.isTTY) {
@@ -843,6 +871,9 @@ async function loop(opts) {
843
871
  process.stdin.on("data", abortHandler);
844
872
  try {
845
873
  types.logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
874
+ if (opts.onProcessStart) {
875
+ opts.onProcessStart("remote");
876
+ }
846
877
  await claudeRemote({
847
878
  abort: remoteAbortController.signal,
848
879
  sessionId,
@@ -856,7 +887,14 @@ async function loop(opts) {
856
887
  claudeEnvVars: opts.claudeEnvVars,
857
888
  claudeArgs: opts.claudeArgs
858
889
  });
890
+ } catch (e) {
891
+ if (!remoteAbortController.signal.aborted) {
892
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
893
+ }
859
894
  } finally {
895
+ if (opts.onProcessStop) {
896
+ opts.onProcessStop("remote");
897
+ }
860
898
  process.stdin.off("data", abortHandler);
861
899
  if (process.stdin.isTTY) {
862
900
  process.stdin.setRawMode(false);
@@ -971,7 +1009,7 @@ class InterruptController {
971
1009
  }
972
1010
  }
973
1011
 
974
- var version = "0.1.11";
1012
+ var version = "0.1.12";
975
1013
  var packageJson = {
976
1014
  version: version};
977
1015
 
@@ -1027,11 +1065,22 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1027
1065
  return;
1028
1066
  }
1029
1067
  session.updateAgentState((currentState) => {
1068
+ const request = currentState.requests?.[id];
1069
+ if (!request) return currentState;
1030
1070
  let r = { ...currentState.requests };
1031
1071
  delete r[id];
1032
1072
  return {
1033
1073
  ...currentState,
1034
- requests: r
1074
+ requests: r,
1075
+ completedRequests: {
1076
+ ...currentState.completedRequests,
1077
+ [id]: {
1078
+ ...request,
1079
+ completedAt: Date.now(),
1080
+ status: message.approved ? "approved" : "denied",
1081
+ reason: message.reason
1082
+ }
1083
+ }
1035
1084
  };
1036
1085
  });
1037
1086
  });
@@ -1287,59 +1336,96 @@ async function startHTTPDirectProxy(options) {
1287
1336
  }
1288
1337
 
1289
1338
  async function startClaudeActivityTracker(onThinking) {
1339
+ types.logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1290
1340
  let requestCounter = 0;
1291
- const activeRequests = /* @__PURE__ */ new Set();
1341
+ const activeRequests = /* @__PURE__ */ new Map();
1292
1342
  let stopThinkingTimeout = null;
1293
1343
  let isThinking = false;
1344
+ const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1345
+ const checkAndStopThinking = () => {
1346
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1347
+ stopThinkingTimeout = setTimeout(() => {
1348
+ if (isThinking && activeRequests.size === 0) {
1349
+ isThinking = false;
1350
+ onThinking(false);
1351
+ }
1352
+ stopThinkingTimeout = null;
1353
+ }, 500);
1354
+ }
1355
+ };
1294
1356
  const proxyUrl = await startHTTPDirectProxy({
1295
1357
  target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1296
1358
  onRequest: (req, proxyReq) => {
1297
1359
  if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1298
1360
  const requestId = ++requestCounter;
1299
- activeRequests.add(requestId);
1300
1361
  req._requestId = requestId;
1301
1362
  if (stopThinkingTimeout) {
1302
1363
  clearTimeout(stopThinkingTimeout);
1303
1364
  stopThinkingTimeout = null;
1304
1365
  }
1366
+ const timeout = setTimeout(() => {
1367
+ activeRequests.delete(requestId);
1368
+ checkAndStopThinking();
1369
+ }, REQUEST_TIMEOUT);
1370
+ activeRequests.set(requestId, timeout);
1305
1371
  if (!isThinking) {
1306
- types.logger.debug(`[ClaudeActivityTracker] Thinking started`);
1307
1372
  isThinking = true;
1308
1373
  onThinking(true);
1309
1374
  }
1310
1375
  }
1311
1376
  },
1312
1377
  onResponse: (req, proxyRes) => {
1378
+ proxyRes.headers["connection"] = "close";
1313
1379
  if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1314
1380
  const requestId = req._requestId;
1315
- proxyRes.on("end", () => {
1316
- activeRequests.delete(requestId);
1317
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1318
- stopThinkingTimeout = setTimeout(() => {
1319
- if (isThinking) {
1320
- isThinking = false;
1321
- types.logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1322
- onThinking(false);
1323
- }
1324
- }, 500);
1381
+ const timeout = activeRequests.get(requestId);
1382
+ if (timeout) {
1383
+ clearTimeout(timeout);
1384
+ }
1385
+ let cleaned = false;
1386
+ const cleanupRequest = () => {
1387
+ if (!cleaned) {
1388
+ cleaned = true;
1389
+ activeRequests.delete(requestId);
1390
+ checkAndStopThinking();
1325
1391
  }
1392
+ };
1393
+ proxyRes.on("end", () => {
1394
+ cleanupRequest();
1326
1395
  });
1327
- proxyRes.on("error", () => {
1328
- activeRequests.delete(requestId);
1329
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1330
- stopThinkingTimeout = setTimeout(() => {
1331
- if (isThinking) {
1332
- isThinking = false;
1333
- types.logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1334
- onThinking(false);
1335
- }
1336
- }, 500);
1337
- }
1396
+ proxyRes.on("error", (err) => {
1397
+ cleanupRequest();
1398
+ });
1399
+ proxyRes.on("aborted", () => {
1400
+ cleanupRequest();
1401
+ });
1402
+ proxyRes.on("close", () => {
1403
+ cleanupRequest();
1404
+ });
1405
+ req.on("close", () => {
1406
+ cleanupRequest();
1338
1407
  });
1339
1408
  }
1340
1409
  }
1341
1410
  });
1342
- return proxyUrl;
1411
+ const reset = () => {
1412
+ for (const [requestId, timeout] of activeRequests) {
1413
+ clearTimeout(timeout);
1414
+ }
1415
+ activeRequests.clear();
1416
+ if (stopThinkingTimeout) {
1417
+ clearTimeout(stopThinkingTimeout);
1418
+ stopThinkingTimeout = null;
1419
+ }
1420
+ if (isThinking) {
1421
+ isThinking = false;
1422
+ onThinking(false);
1423
+ }
1424
+ };
1425
+ return {
1426
+ proxyUrl,
1427
+ reset
1428
+ };
1343
1429
  }
1344
1430
 
1345
1431
  async function start(credentials, options = {}) {
@@ -1357,11 +1443,11 @@ async function start(credentials, options = {}) {
1357
1443
  let pingInterval = setInterval(() => {
1358
1444
  session.keepAlive(thinking, mode);
1359
1445
  }, 2e3);
1360
- const proxyUrl = await startClaudeActivityTracker((newThinking) => {
1446
+ const activityTracker = await startClaudeActivityTracker((newThinking) => {
1361
1447
  thinking = newThinking;
1362
1448
  session.keepAlive(thinking, mode);
1363
1449
  });
1364
- process.env.ANTHROPIC_BASE_URL = proxyUrl;
1450
+ process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1365
1451
  const logPath = await types.logger.logFilePathPromise;
1366
1452
  types.logger.infoDeveloper(`Session: ${response.id}`);
1367
1453
  types.logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1380,11 +1466,22 @@ async function start(credentials, options = {}) {
1380
1466
  }
1381
1467
  requests.delete(id);
1382
1468
  session.updateAgentState((currentState) => {
1469
+ const request2 = currentState.requests?.[id];
1470
+ if (!request2) return currentState;
1383
1471
  let r = { ...currentState.requests };
1384
1472
  delete r[id];
1385
1473
  return {
1386
1474
  ...currentState,
1387
- requests: r
1475
+ requests: r,
1476
+ completedRequests: {
1477
+ ...currentState.completedRequests,
1478
+ [id]: {
1479
+ ...request2,
1480
+ completedAt: Date.now(),
1481
+ status: "canceled",
1482
+ reason: "Timeout"
1483
+ }
1484
+ }
1388
1485
  };
1389
1486
  });
1390
1487
  }, 1e3 * 60 * 4.5);
@@ -1410,7 +1507,8 @@ async function start(credentials, options = {}) {
1410
1507
  ...currentState.requests,
1411
1508
  [id]: {
1412
1509
  tool: request.name,
1413
- arguments: request.arguments
1510
+ arguments: request.arguments,
1511
+ createdAt: Date.now()
1414
1512
  }
1415
1513
  }
1416
1514
  }));
@@ -1446,10 +1544,58 @@ async function start(credentials, options = {}) {
1446
1544
  mode = newMode;
1447
1545
  session.sendSessionEvent({ type: "switch", mode: newMode });
1448
1546
  session.keepAlive(thinking, mode);
1449
- session.updateAgentState((currentState) => ({
1450
- ...currentState,
1451
- controlledByUser: newMode === "local" ? true : false
1452
- }));
1547
+ if (newMode === "local") {
1548
+ types.logger.debug("Switching to local mode - clearing pending permission requests");
1549
+ for (const [id, resolve] of requests) {
1550
+ types.logger.debug(`Rejecting pending permission request: ${id}`);
1551
+ resolve({ approved: false, reason: "Session switched to local mode" });
1552
+ }
1553
+ requests.clear();
1554
+ session.updateAgentState((currentState) => {
1555
+ const pendingRequests = currentState.requests || {};
1556
+ const completedRequests = { ...currentState.completedRequests };
1557
+ for (const [id, request] of Object.entries(pendingRequests)) {
1558
+ completedRequests[id] = {
1559
+ ...request,
1560
+ completedAt: Date.now(),
1561
+ status: "canceled",
1562
+ reason: "Session switched to local mode"
1563
+ };
1564
+ }
1565
+ return {
1566
+ ...currentState,
1567
+ controlledByUser: true,
1568
+ requests: {},
1569
+ // Clear all pending requests
1570
+ completedRequests
1571
+ };
1572
+ });
1573
+ } else {
1574
+ session.updateAgentState((currentState) => ({
1575
+ ...currentState,
1576
+ controlledByUser: false
1577
+ }));
1578
+ }
1579
+ },
1580
+ onProcessStart: (processMode) => {
1581
+ types.logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1582
+ activityTracker.reset();
1583
+ types.logger.debug("Starting process - clearing any stale permission requests");
1584
+ for (const [id, resolve] of requests) {
1585
+ types.logger.debug(`Rejecting stale permission request: ${id}`);
1586
+ resolve({ approved: false, reason: "Process restarted" });
1587
+ }
1588
+ requests.clear();
1589
+ },
1590
+ onProcessStop: (processMode) => {
1591
+ types.logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1592
+ activityTracker.reset();
1593
+ types.logger.debug("Stopping process - clearing any stale permission requests");
1594
+ for (const [id, resolve] of requests) {
1595
+ types.logger.debug(`Rejecting stale permission request: ${id}`);
1596
+ resolve({ approved: false, reason: "Process restarted" });
1597
+ }
1598
+ requests.clear();
1453
1599
  },
1454
1600
  mcpServers: {
1455
1601
  "permission": {
@@ -2068,9 +2214,7 @@ Currently only supported on macOS.
2068
2214
  options.model = args[++i];
2069
2215
  } else if (arg === "-p" || arg === "--permission-mode") {
2070
2216
  options.permissionMode = z.z.enum(["auto", "default", "plan"]).parse(args[++i]);
2071
- } else if (arg === "--local") {
2072
- i++;
2073
- } else if (arg === "--happy-starting-mode") {
2217
+ } else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
2074
2218
  options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
2075
2219
  } else if (arg === "--claude-env") {
2076
2220
  const envVar = args[++i];
package/dist/index.mjs CHANGED
@@ -243,7 +243,7 @@ async function claudeRemote(opts) {
243
243
  try {
244
244
  logger.debug(`[claudeRemote] Starting to iterate over response`);
245
245
  for await (const message of response) {
246
- logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
246
+ logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
247
247
  formatClaudeMessage(message, opts.onAssistantResult);
248
248
  if (message.type === "system" && message.subtype === "init") {
249
249
  logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
@@ -579,8 +579,8 @@ function createSessionScanner(opts) {
579
579
  let pendingSessions = /* @__PURE__ */ new Set();
580
580
  let currentSessionId = null;
581
581
  let watchers = /* @__PURE__ */ new Map();
582
- let processedMessages = /* @__PURE__ */ new Set();
583
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
582
+ let processedMessageKeys = /* @__PURE__ */ new Set();
583
+ let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
584
584
  const sync = new InvalidateSync(async () => {
585
585
  logger.debug(`[SESSION_SCANNER] Syncing...`);
586
586
  let sessions = [];
@@ -612,16 +612,16 @@ function createSessionScanner(opts) {
612
612
  continue;
613
613
  }
614
614
  let key = getMessageKey(parsed.data);
615
- if (processedMessages.has(key)) {
615
+ if (processedMessageKeys.has(key)) {
616
616
  continue;
617
617
  }
618
- processedMessages.add(key);
618
+ processedMessageKeys.add(key);
619
619
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
620
620
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
621
621
  if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
622
- const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
623
- if (currentCounter && currentCounter > 0) {
624
- seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
622
+ if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
623
+ logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
624
+ unmatchedServerMessageContents.delete(parsed.data.message.content);
625
625
  continue;
626
626
  }
627
627
  }
@@ -682,13 +682,18 @@ function createSessionScanner(opts) {
682
682
  sync.invalidate();
683
683
  },
684
684
  onRemoteUserMessageForDeduplication: (messageContent) => {
685
- seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
685
+ logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
686
+ unmatchedServerMessageContents.add(messageContent);
686
687
  }
687
688
  };
688
689
  }
689
690
  function getMessageKey(message) {
690
691
  if (message.type === "user") {
691
- return `user:${message.uuid}`;
692
+ if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
693
+ return `user-message-content:${stableStringify(message.message.content[0].text)}`;
694
+ } else {
695
+ return `user-message-content:${stableStringify(message.message.content)}`;
696
+ }
692
697
  } else if (message.type === "assistant") {
693
698
  const { usage, ...messageWithoutUsage } = message.message;
694
699
  return stableStringify(messageWithoutUsage);
@@ -700,6 +705,9 @@ function getMessageKey(message) {
700
705
  return `unknown:<error, this should be unreachable>`;
701
706
  }
702
707
  function stableStringify(obj) {
708
+ if (!obj) {
709
+ return "null";
710
+ }
703
711
  return JSON.stringify(sortKeys(obj), null, 2);
704
712
  }
705
713
  function sortKeys(value) {
@@ -763,6 +771,11 @@ async function loop(opts) {
763
771
  interactiveAbortController.abort();
764
772
  }
765
773
  });
774
+ opts.session.setHandler("abort", () => {
775
+ if (onMessage) {
776
+ onMessage();
777
+ }
778
+ });
766
779
  onMessage = () => {
767
780
  if (!interactiveAbortController.signal.aborted) {
768
781
  abortedOutside = true;
@@ -772,18 +785,32 @@ async function loop(opts) {
772
785
  opts.onModeChange(mode);
773
786
  }
774
787
  }
788
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
775
789
  interactiveAbortController.abort();
776
790
  }
777
791
  onMessage = null;
778
792
  };
779
- await claudeLocal({
780
- path: opts.path,
781
- sessionId,
782
- onSessionFound,
783
- abort: interactiveAbortController.signal,
784
- claudeEnvVars: opts.claudeEnvVars,
785
- claudeArgs: opts.claudeArgs
786
- });
793
+ try {
794
+ if (opts.onProcessStart) {
795
+ opts.onProcessStart("local");
796
+ }
797
+ await claudeLocal({
798
+ path: opts.path,
799
+ sessionId,
800
+ onSessionFound,
801
+ abort: interactiveAbortController.signal,
802
+ claudeEnvVars: opts.claudeEnvVars,
803
+ claudeArgs: opts.claudeArgs
804
+ });
805
+ } catch (e) {
806
+ if (!interactiveAbortController.signal.aborted) {
807
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
808
+ }
809
+ } finally {
810
+ if (opts.onProcessStop) {
811
+ opts.onProcessStop("local");
812
+ }
813
+ }
787
814
  onMessage = null;
788
815
  if (!abortedOutside) {
789
816
  return;
@@ -808,6 +835,7 @@ async function loop(opts) {
808
835
  opts.onModeChange(mode);
809
836
  }
810
837
  }
838
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
811
839
  remoteAbortController.abort();
812
840
  }
813
841
  if (process.stdin.isTTY) {
@@ -822,6 +850,9 @@ async function loop(opts) {
822
850
  process.stdin.on("data", abortHandler);
823
851
  try {
824
852
  logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
853
+ if (opts.onProcessStart) {
854
+ opts.onProcessStart("remote");
855
+ }
825
856
  await claudeRemote({
826
857
  abort: remoteAbortController.signal,
827
858
  sessionId,
@@ -835,7 +866,14 @@ async function loop(opts) {
835
866
  claudeEnvVars: opts.claudeEnvVars,
836
867
  claudeArgs: opts.claudeArgs
837
868
  });
869
+ } catch (e) {
870
+ if (!remoteAbortController.signal.aborted) {
871
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
872
+ }
838
873
  } finally {
874
+ if (opts.onProcessStop) {
875
+ opts.onProcessStop("remote");
876
+ }
839
877
  process.stdin.off("data", abortHandler);
840
878
  if (process.stdin.isTTY) {
841
879
  process.stdin.setRawMode(false);
@@ -950,7 +988,7 @@ class InterruptController {
950
988
  }
951
989
  }
952
990
 
953
- var version = "0.1.11";
991
+ var version = "0.1.12";
954
992
  var packageJson = {
955
993
  version: version};
956
994
 
@@ -1006,11 +1044,22 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1006
1044
  return;
1007
1045
  }
1008
1046
  session.updateAgentState((currentState) => {
1047
+ const request = currentState.requests?.[id];
1048
+ if (!request) return currentState;
1009
1049
  let r = { ...currentState.requests };
1010
1050
  delete r[id];
1011
1051
  return {
1012
1052
  ...currentState,
1013
- requests: r
1053
+ requests: r,
1054
+ completedRequests: {
1055
+ ...currentState.completedRequests,
1056
+ [id]: {
1057
+ ...request,
1058
+ completedAt: Date.now(),
1059
+ status: message.approved ? "approved" : "denied",
1060
+ reason: message.reason
1061
+ }
1062
+ }
1014
1063
  };
1015
1064
  });
1016
1065
  });
@@ -1266,59 +1315,96 @@ async function startHTTPDirectProxy(options) {
1266
1315
  }
1267
1316
 
1268
1317
  async function startClaudeActivityTracker(onThinking) {
1318
+ logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1269
1319
  let requestCounter = 0;
1270
- const activeRequests = /* @__PURE__ */ new Set();
1320
+ const activeRequests = /* @__PURE__ */ new Map();
1271
1321
  let stopThinkingTimeout = null;
1272
1322
  let isThinking = false;
1323
+ const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1324
+ const checkAndStopThinking = () => {
1325
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1326
+ stopThinkingTimeout = setTimeout(() => {
1327
+ if (isThinking && activeRequests.size === 0) {
1328
+ isThinking = false;
1329
+ onThinking(false);
1330
+ }
1331
+ stopThinkingTimeout = null;
1332
+ }, 500);
1333
+ }
1334
+ };
1273
1335
  const proxyUrl = await startHTTPDirectProxy({
1274
1336
  target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1275
1337
  onRequest: (req, proxyReq) => {
1276
1338
  if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1277
1339
  const requestId = ++requestCounter;
1278
- activeRequests.add(requestId);
1279
1340
  req._requestId = requestId;
1280
1341
  if (stopThinkingTimeout) {
1281
1342
  clearTimeout(stopThinkingTimeout);
1282
1343
  stopThinkingTimeout = null;
1283
1344
  }
1345
+ const timeout = setTimeout(() => {
1346
+ activeRequests.delete(requestId);
1347
+ checkAndStopThinking();
1348
+ }, REQUEST_TIMEOUT);
1349
+ activeRequests.set(requestId, timeout);
1284
1350
  if (!isThinking) {
1285
- logger.debug(`[ClaudeActivityTracker] Thinking started`);
1286
1351
  isThinking = true;
1287
1352
  onThinking(true);
1288
1353
  }
1289
1354
  }
1290
1355
  },
1291
1356
  onResponse: (req, proxyRes) => {
1357
+ proxyRes.headers["connection"] = "close";
1292
1358
  if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1293
1359
  const requestId = req._requestId;
1294
- proxyRes.on("end", () => {
1295
- activeRequests.delete(requestId);
1296
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1297
- stopThinkingTimeout = setTimeout(() => {
1298
- if (isThinking) {
1299
- isThinking = false;
1300
- logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1301
- onThinking(false);
1302
- }
1303
- }, 500);
1360
+ const timeout = activeRequests.get(requestId);
1361
+ if (timeout) {
1362
+ clearTimeout(timeout);
1363
+ }
1364
+ let cleaned = false;
1365
+ const cleanupRequest = () => {
1366
+ if (!cleaned) {
1367
+ cleaned = true;
1368
+ activeRequests.delete(requestId);
1369
+ checkAndStopThinking();
1304
1370
  }
1371
+ };
1372
+ proxyRes.on("end", () => {
1373
+ cleanupRequest();
1305
1374
  });
1306
- proxyRes.on("error", () => {
1307
- activeRequests.delete(requestId);
1308
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1309
- stopThinkingTimeout = setTimeout(() => {
1310
- if (isThinking) {
1311
- isThinking = false;
1312
- logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1313
- onThinking(false);
1314
- }
1315
- }, 500);
1316
- }
1375
+ proxyRes.on("error", (err) => {
1376
+ cleanupRequest();
1377
+ });
1378
+ proxyRes.on("aborted", () => {
1379
+ cleanupRequest();
1380
+ });
1381
+ proxyRes.on("close", () => {
1382
+ cleanupRequest();
1383
+ });
1384
+ req.on("close", () => {
1385
+ cleanupRequest();
1317
1386
  });
1318
1387
  }
1319
1388
  }
1320
1389
  });
1321
- return proxyUrl;
1390
+ const reset = () => {
1391
+ for (const [requestId, timeout] of activeRequests) {
1392
+ clearTimeout(timeout);
1393
+ }
1394
+ activeRequests.clear();
1395
+ if (stopThinkingTimeout) {
1396
+ clearTimeout(stopThinkingTimeout);
1397
+ stopThinkingTimeout = null;
1398
+ }
1399
+ if (isThinking) {
1400
+ isThinking = false;
1401
+ onThinking(false);
1402
+ }
1403
+ };
1404
+ return {
1405
+ proxyUrl,
1406
+ reset
1407
+ };
1322
1408
  }
1323
1409
 
1324
1410
  async function start(credentials, options = {}) {
@@ -1336,11 +1422,11 @@ async function start(credentials, options = {}) {
1336
1422
  let pingInterval = setInterval(() => {
1337
1423
  session.keepAlive(thinking, mode);
1338
1424
  }, 2e3);
1339
- const proxyUrl = await startClaudeActivityTracker((newThinking) => {
1425
+ const activityTracker = await startClaudeActivityTracker((newThinking) => {
1340
1426
  thinking = newThinking;
1341
1427
  session.keepAlive(thinking, mode);
1342
1428
  });
1343
- process.env.ANTHROPIC_BASE_URL = proxyUrl;
1429
+ process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1344
1430
  const logPath = await logger.logFilePathPromise;
1345
1431
  logger.infoDeveloper(`Session: ${response.id}`);
1346
1432
  logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1359,11 +1445,22 @@ async function start(credentials, options = {}) {
1359
1445
  }
1360
1446
  requests.delete(id);
1361
1447
  session.updateAgentState((currentState) => {
1448
+ const request2 = currentState.requests?.[id];
1449
+ if (!request2) return currentState;
1362
1450
  let r = { ...currentState.requests };
1363
1451
  delete r[id];
1364
1452
  return {
1365
1453
  ...currentState,
1366
- requests: r
1454
+ requests: r,
1455
+ completedRequests: {
1456
+ ...currentState.completedRequests,
1457
+ [id]: {
1458
+ ...request2,
1459
+ completedAt: Date.now(),
1460
+ status: "canceled",
1461
+ reason: "Timeout"
1462
+ }
1463
+ }
1367
1464
  };
1368
1465
  });
1369
1466
  }, 1e3 * 60 * 4.5);
@@ -1389,7 +1486,8 @@ async function start(credentials, options = {}) {
1389
1486
  ...currentState.requests,
1390
1487
  [id]: {
1391
1488
  tool: request.name,
1392
- arguments: request.arguments
1489
+ arguments: request.arguments,
1490
+ createdAt: Date.now()
1393
1491
  }
1394
1492
  }
1395
1493
  }));
@@ -1425,10 +1523,58 @@ async function start(credentials, options = {}) {
1425
1523
  mode = newMode;
1426
1524
  session.sendSessionEvent({ type: "switch", mode: newMode });
1427
1525
  session.keepAlive(thinking, mode);
1428
- session.updateAgentState((currentState) => ({
1429
- ...currentState,
1430
- controlledByUser: newMode === "local" ? true : false
1431
- }));
1526
+ if (newMode === "local") {
1527
+ logger.debug("Switching to local mode - clearing pending permission requests");
1528
+ for (const [id, resolve] of requests) {
1529
+ logger.debug(`Rejecting pending permission request: ${id}`);
1530
+ resolve({ approved: false, reason: "Session switched to local mode" });
1531
+ }
1532
+ requests.clear();
1533
+ session.updateAgentState((currentState) => {
1534
+ const pendingRequests = currentState.requests || {};
1535
+ const completedRequests = { ...currentState.completedRequests };
1536
+ for (const [id, request] of Object.entries(pendingRequests)) {
1537
+ completedRequests[id] = {
1538
+ ...request,
1539
+ completedAt: Date.now(),
1540
+ status: "canceled",
1541
+ reason: "Session switched to local mode"
1542
+ };
1543
+ }
1544
+ return {
1545
+ ...currentState,
1546
+ controlledByUser: true,
1547
+ requests: {},
1548
+ // Clear all pending requests
1549
+ completedRequests
1550
+ };
1551
+ });
1552
+ } else {
1553
+ session.updateAgentState((currentState) => ({
1554
+ ...currentState,
1555
+ controlledByUser: false
1556
+ }));
1557
+ }
1558
+ },
1559
+ onProcessStart: (processMode) => {
1560
+ logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1561
+ activityTracker.reset();
1562
+ logger.debug("Starting process - clearing any stale permission requests");
1563
+ for (const [id, resolve] of requests) {
1564
+ logger.debug(`Rejecting stale permission request: ${id}`);
1565
+ resolve({ approved: false, reason: "Process restarted" });
1566
+ }
1567
+ requests.clear();
1568
+ },
1569
+ onProcessStop: (processMode) => {
1570
+ logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1571
+ activityTracker.reset();
1572
+ logger.debug("Stopping process - clearing any stale permission requests");
1573
+ for (const [id, resolve] of requests) {
1574
+ logger.debug(`Rejecting stale permission request: ${id}`);
1575
+ resolve({ approved: false, reason: "Process restarted" });
1576
+ }
1577
+ requests.clear();
1432
1578
  },
1433
1579
  mcpServers: {
1434
1580
  "permission": {
@@ -2047,9 +2193,7 @@ Currently only supported on macOS.
2047
2193
  options.model = args[++i];
2048
2194
  } else if (arg === "-p" || arg === "--permission-mode") {
2049
2195
  options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
2050
- } else if (arg === "--local") {
2051
- i++;
2052
- } else if (arg === "--happy-starting-mode") {
2196
+ } else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
2053
2197
  options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
2054
2198
  } else if (arg === "--claude-env") {
2055
2199
  const envVar = args[++i];
package/dist/lib.d.cts CHANGED
@@ -369,6 +369,17 @@ type AgentState = {
369
369
  [id: string]: {
370
370
  tool: string;
371
371
  arguments: any;
372
+ createdAt: number;
373
+ };
374
+ };
375
+ completedRequests?: {
376
+ [id: string]: {
377
+ tool: string;
378
+ arguments: any;
379
+ createdAt: number;
380
+ completedAt: number;
381
+ status: 'canceled' | 'denied' | 'approved';
382
+ reason?: string;
372
383
  };
373
384
  };
374
385
  };
@@ -396,6 +407,9 @@ declare class ApiSessionClient extends EventEmitter {
396
407
  sendSessionEvent(event: {
397
408
  type: 'switch';
398
409
  mode: 'local' | 'remote';
410
+ } | {
411
+ type: 'message';
412
+ message: string;
399
413
  }, id?: string): void;
400
414
  /**
401
415
  * Send a ping message to keep the connection alive
package/dist/lib.d.mts CHANGED
@@ -369,6 +369,17 @@ type AgentState = {
369
369
  [id: string]: {
370
370
  tool: string;
371
371
  arguments: any;
372
+ createdAt: number;
373
+ };
374
+ };
375
+ completedRequests?: {
376
+ [id: string]: {
377
+ tool: string;
378
+ arguments: any;
379
+ createdAt: number;
380
+ completedAt: number;
381
+ status: 'canceled' | 'denied' | 'approved';
382
+ reason?: string;
372
383
  };
373
384
  };
374
385
  };
@@ -396,6 +407,9 @@ declare class ApiSessionClient extends EventEmitter {
396
407
  sendSessionEvent(event: {
397
408
  type: 'switch';
398
409
  mode: 'local' | 'remote';
410
+ } | {
411
+ type: 'message';
412
+ message: string;
399
413
  }, id?: string): void;
400
414
  /**
401
415
  * Send a ping message to keep the connection alive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "dev:local-server": "HANDY_SERVER_URL=http://localhost:3005 npx tsx --env-file .env.sample src/index.ts"
53
53
  },
54
54
  "dependencies": {
55
- "@anthropic-ai/claude-code": "^1.0.51",
55
+ "@anthropic-ai/claude-code": "^1.0.68",
56
56
  "@anthropic-ai/sdk": "^0.56.0",
57
57
  "@modelcontextprotocol/sdk": "^1.15.1",
58
58
  "@stablelib/base64": "^2.0.1",