voicesmith-mcp 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicesmith-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Local AI voice for coding assistants — TTS & STT via MCP. Kokoro ONNX + faster-whisper, fully offline.",
5
5
  "bin": {
6
6
  "voicesmith-mcp": "bin/cli.js"
package/server.py CHANGED
@@ -206,7 +206,8 @@ class _VoiceHTTPHandler(BaseHTTPRequestHandler):
206
206
  self._json_response(400, {"error": "invalid_json"})
207
207
  return
208
208
 
209
- name = params.get("name", _session_info.get("name", "Eric") if _session_info else "Eric")
209
+ default_name = _config.main_agent if _config else "Eric"
210
+ name = params.get("name", _session_info.get("name", default_name) if _session_info else default_name)
210
211
  text = params.get("text", "")
211
212
  speed = params.get("speed", 1.0)
212
213
 
@@ -806,12 +807,28 @@ def main():
806
807
 
807
808
 
808
809
  def _start_preheat_intro():
809
- """Speak a brief intro after server starts. Preheats TTS engine."""
810
+ """Speak a brief intro after server starts. Preheats TTS engine.
811
+
812
+ Only speaks if this session got its preferred name. If the preferred name
813
+ was taken (e.g., during a session resume where the old server is still
814
+ running), skip the intro to avoid confusing double introductions.
815
+ """
810
816
  if _tts_engine is None or _audio_player is None:
811
817
  return
812
818
 
813
- name = _session_info.get("name", "Eric") if _session_info else "Eric"
814
- voice = _session_info.get("voice", "am_eric") if _session_info else "am_eric"
819
+ default_name = _config.main_agent if _config else "Eric"
820
+ default_voice = _config.tts.default_voice if _config else "am_eric"
821
+
822
+ name = _session_info.get("name", default_name) if _session_info else default_name
823
+ voice = _session_info.get("voice", default_voice) if _session_info else default_voice
824
+
825
+ # Determine what name we wanted
826
+ preferred = (_config.last_voice_name or default_name) if _config else default_name
827
+
828
+ # Skip intro if we didn't get our preferred name — another session has it
829
+ if name != preferred:
830
+ logger.info(f"Skipping preheat intro: wanted '{preferred}' but got '{name}'")
831
+ return
815
832
 
816
833
  def _intro():
817
834
  # Wait for server to settle
@@ -65,7 +65,7 @@ def _write_sessions(path: Path, sessions: list[dict]) -> None:
65
65
  _STALE_ACTIVITY_THRESHOLD = 300 # 5 minutes
66
66
 
67
67
 
68
- def _session_healthy(session: dict) -> bool:
68
+ def _session_healthy(session: dict, activity_threshold: int = _STALE_ACTIVITY_THRESHOLD) -> bool:
69
69
  """Check if a session is alive and actively used.
70
70
 
71
71
  Three checks:
@@ -97,7 +97,7 @@ def _session_healthy(session: dict) -> bool:
97
97
  # Check activity age — if server hasn't had MCP tool calls
98
98
  # in a while, it's orphaned
99
99
  age = data.get("last_tool_call_age_s")
100
- if age is not None and age > _STALE_ACTIVITY_THRESHOLD:
100
+ if age is not None and age > activity_threshold:
101
101
  logger.info(
102
102
  f"Session '{session.get('name')}' (pid {pid}) inactive "
103
103
  f"for {age}s — treating as stale"
@@ -117,11 +117,18 @@ def _session_healthy(session: dict) -> bool:
117
117
  return False
118
118
 
119
119
 
120
- def _clean_stale(sessions: list[dict]) -> list[dict]:
121
- """Remove sessions that are dead or unresponsive."""
120
+ def _clean_stale(sessions: list[dict], aggressive: bool = False) -> list[dict]:
121
+ """Remove sessions that are dead or unresponsive.
122
+
123
+ Args:
124
+ aggressive: If True, use a shorter activity threshold (10s instead of 5min).
125
+ Used during startup registration to quickly reclaim names from
126
+ orphaned servers that haven't fully shut down yet.
127
+ """
128
+ threshold = 10 if aggressive else _STALE_ACTIVITY_THRESHOLD
122
129
  alive = []
123
130
  for s in sessions:
124
- if _session_healthy(s):
131
+ if _session_healthy(s, activity_threshold=threshold):
125
132
  alive.append(s)
126
133
  else:
127
134
  logger.info(f"Removed stale session: {s.get('name')} (pid {s.get('pid')})")
@@ -214,10 +221,22 @@ def register_session(
214
221
  fcntl.flock(f, fcntl.LOCK_EX)
215
222
 
216
223
  sessions = _read_sessions(path)
217
- sessions = _clean_stale(sessions)
224
+ # Aggressive cleanup on startup — use short activity threshold
225
+ # to quickly reclaim names from orphaned servers
226
+ sessions = _clean_stale(sessions, aggressive=True)
218
227
 
219
228
  taken_names = {s["name"] for s in sessions}
220
229
 
230
+ if preferred_name in taken_names:
231
+ # Wait briefly and retry — the old server may be shutting down
232
+ fcntl.flock(f, fcntl.LOCK_UN)
233
+ time.sleep(2)
234
+ fcntl.flock(f, fcntl.LOCK_EX)
235
+ sessions = _read_sessions(path)
236
+ sessions = _clean_stale(sessions, aggressive=True)
237
+ _write_sessions(path, sessions)
238
+ taken_names = {s["name"] for s in sessions}
239
+
221
240
  if preferred_name in taken_names:
222
241
  name, voice = _find_available_name(taken_names, preferred_name)
223
242
  logger.warning(