Skip to content

Commit 44303ae

Browse files
committed
Merge remote-tracking branch 'origin/main' into e2e-flow
2 parents d8dc62a + 7b457aa commit 44303ae

86 files changed

Lines changed: 6178 additions & 2439 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/memory.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Quick reference for anyone starting with Claude on this project. Updated by the
8787
- **Banner dismiss state uses localStorage** (prefix `openhuman:upsell:`), not Redux — consistent with CLAUDE.md exception for ephemeral UI state.
8888
- **Phased rollout** — Phase 1 = banners + limit modal + hook. Phase 2 = onboarding upsell + analytics. Phase 3 = remote config + A/B testing.
8989
- **"5-hour" label stragglers in Conversations.tsx**`LimitPill` label and its hover tooltip still say "5h" / "5-hour". Commit 8c52236's "10-hour" terminology refactor missed those two spots.
90+
- **`getTeamUsage()` now normalizes via `normalizeTeamUsage()`** — Added in issue #482. The Rust sidecar passes backend JSON through opaquely (`src/openhuman/team/ops.rs`), so the TS client must normalize field names and types. Pattern matches existing `normalizeCreditBalance()` in the same file. Any new billing API that returns raw backend data should follow the same normalize-at-the-client pattern.
91+
- **Two separate `TeamUsage` types exist**`creditsApi.ts:24` (billing: cycle budget, limits) and `types/team.ts:11` (team model: daily token limit). Different import paths, no collision, but confusing.
9092

9193
## Settings & Skills Reorganization (Issue #396)
9294

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ src-tauri/target/
5959

6060
workflow
6161
.fastembed_cache
62+
overlay/src-tauri/target/

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openhuman"
3-
version = "0.52.0"
3+
version = "0.52.2"
44
edition = "2021"
55
description = "OpenHuman core business logic and RPC server"
66
autobins = false

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openhuman-app",
3-
"version": "0.52.0",
3+
"version": "0.52.2",
44
"type": "module",
55
"scripts": {
66
"dev": "vite",

app/src-tauri/Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src-tauri/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "OpenHuman"
3-
version = "0.52.0"
3+
version = "0.52.2"
44
description = "OpenHuman - AI-powered Super Assistant"
55
authors = ["OpenHuman"]
66
edition = "2021"
@@ -40,6 +40,9 @@ semver = "1"
4040
log = "0.4"
4141
env_logger = "0.11"
4242

43+
[target.'cfg(unix)'.dependencies]
44+
nix = { version = "0.29", default-features = false, features = ["signal"] }
45+
4346
[target.'cfg(target_os = "macos")'.dependencies]
4447
objc2-app-kit = "0.3.2"
4548
objc2-core-graphics = "0.3.2"

app/src-tauri/Info.plist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSMicrophoneUsageDescription</key>
6+
<string>OpenHuman uses the microphone for voice dictation — press the hotkey to record speech and transcribe it to text.</string>
7+
</dict>
8+
</plist>

app/src-tauri/entitlements.sidecar.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,16 @@
88
<true/>
99
<key>com.apple.security.cs.disable-library-validation</key>
1010
<true/>
11+
<key>com.apple.security.device.audio-input</key>
12+
<!-- Required for the core sidecar to make outbound HTTPS calls (registry fetch,
13+
skill manifest + JS bundle downloads) under the macOS Hardened Runtime.
14+
Without this, reqwest connections are silently blocked by the OS in signed
15+
DMG builds, causing skills_install to appear stuck for ~30 s per request. -->
16+
<key>com.apple.security.network.client</key>
17+
<true/>
18+
<!-- Required for the core sidecar to bind and accept connections on port 7788
19+
(JSON-RPC server, Socket.IO) under the macOS Hardened Runtime. -->
20+
<key>com.apple.security.network.server</key>
21+
<true/>
1122
</dict>
1223
</plist>

app/src-tauri/src/core_process.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ impl CoreProcessHandle {
108108
"[core] found existing core rpc endpoint at {}",
109109
self.rpc_url()
110110
);
111+
log::warn!(
112+
"[core] reusing port {} — if channel/Telegram behavior mismatches the app, another stale `openhuman` core may be attached; check [core-update] logs for version skew.",
113+
self.port
114+
);
111115
return Ok(());
112116
}
113117

@@ -310,26 +314,35 @@ impl CoreProcessHandle {
310314

311315
/// Stop the core process this handle spawned (child or in-process task). Safe to call if
312316
/// nothing was spawned or core was already external.
317+
///
318+
/// On Unix, sends SIGTERM first so the core process can run its graceful
319+
/// shutdown hooks (e.g. stopping the autocomplete engine and its Swift
320+
/// overlay helper). Falls back to SIGKILL after a timeout.
313321
pub async fn shutdown(&self) {
314322
let mut child_guard = self.child.lock().await;
315323
if let Some(mut child) = child_guard.take() {
316324
log::info!("[core] terminating child core process on app shutdown");
317-
if let Err(e) = child.kill().await {
318-
log::warn!("[core] failed to kill child core process: {e}");
325+
326+
let exited = self.try_graceful_terminate(&child).await;
327+
328+
if !exited {
329+
log::info!("[core] graceful shutdown timed out, sending SIGKILL");
330+
if let Err(e) = child.kill().await {
331+
log::warn!("[core] failed to kill child core process: {e}");
332+
}
319333
}
334+
320335
// Wait for the process to exit so the RPC listen socket is released before restart
321336
// checks the port (otherwise we can spuriously hit "port still in use").
322337
match timeout(Duration::from_secs(12), child.wait()).await {
323338
Ok(Ok(status)) => {
324-
log::debug!("[core] child core process reaped after kill: {status}");
339+
log::debug!("[core] child core process reaped after shutdown: {status}");
325340
}
326341
Ok(Err(e)) => {
327-
log::warn!("[core] wait on child core process after kill: {e}");
342+
log::warn!("[core] wait on child core process after shutdown: {e}");
328343
}
329344
Err(_) => {
330-
log::warn!(
331-
"[core] timed out waiting for child core process to exit after kill (12s)"
332-
);
345+
log::warn!("[core] timed out waiting for child core process to exit (12s)");
333346
}
334347
}
335348
}
@@ -338,6 +351,62 @@ impl CoreProcessHandle {
338351
task.abort();
339352
}
340353
}
354+
355+
/// Send SIGTERM to the child and wait up to 5 seconds for it to exit.
356+
/// Returns `true` if the process exited gracefully, `false` if it's still
357+
/// alive (caller should escalate to SIGKILL).
358+
async fn try_graceful_terminate(&self, child: &Child) -> bool {
359+
#[cfg(unix)]
360+
{
361+
use nix::sys::signal::{self, Signal};
362+
use nix::unistd::Pid;
363+
364+
let Some(pid) = child.id() else {
365+
log::debug!("[core] child has no PID (already exited?)");
366+
return true;
367+
};
368+
369+
log::info!("[core] sending SIGTERM to core process (pid={pid})");
370+
if let Err(e) = signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
371+
log::warn!("[core] failed to send SIGTERM: {e}");
372+
return false;
373+
}
374+
375+
// Poll for exit for up to 5 seconds.
376+
const GRACE_PERIOD: Duration = Duration::from_secs(5);
377+
const POLL_INTERVAL: Duration = Duration::from_millis(100);
378+
let start = tokio::time::Instant::now();
379+
380+
while start.elapsed() < GRACE_PERIOD {
381+
// Check if process is still alive (signal 0 = existence check).
382+
match signal::kill(Pid::from_raw(pid as i32), None) {
383+
Err(nix::errno::Errno::ESRCH) => {
384+
log::info!(
385+
"[core] core process exited gracefully after SIGTERM ({}ms)",
386+
start.elapsed().as_millis()
387+
);
388+
return true;
389+
}
390+
_ => {}
391+
}
392+
tokio::time::sleep(POLL_INTERVAL).await;
393+
}
394+
395+
log::warn!(
396+
"[core] core process still alive after {}s grace period",
397+
GRACE_PERIOD.as_secs()
398+
);
399+
false
400+
}
401+
402+
#[cfg(not(unix))]
403+
{
404+
// On non-Unix platforms, there is no SIGTERM equivalent; the caller
405+
// will use `child.kill()` directly.
406+
let _ = child;
407+
false
408+
}
409+
}
341410
}
342411

343412
fn is_current_exe_path(candidate: &std::path::Path) -> bool {

0 commit comments

Comments
 (0)