livepilot 1.10.5 → 1.10.6

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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +50 -0
  4. package/livepilot/.Codex-plugin/plugin.json +1 -1
  5. package/livepilot/.claude-plugin/plugin.json +1 -1
  6. package/livepilot/skills/livepilot-core/references/overview.md +2 -2
  7. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  8. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  9. package/m4l_device/livepilot_bridge.js +12 -1
  10. package/manifest.json +1 -1
  11. package/mcp_server/__init__.py +1 -1
  12. package/mcp_server/composer/sample_resolver.py +10 -6
  13. package/mcp_server/composer/tools.py +10 -6
  14. package/mcp_server/connection.py +6 -1
  15. package/mcp_server/creative_constraints/tools.py +9 -8
  16. package/mcp_server/experiment/engine.py +9 -5
  17. package/mcp_server/experiment/tools.py +9 -9
  18. package/mcp_server/hook_hunter/tools.py +14 -9
  19. package/mcp_server/m4l_bridge.py +11 -0
  20. package/mcp_server/memory/taste_graph.py +7 -2
  21. package/mcp_server/mix_engine/tools.py +8 -3
  22. package/mcp_server/musical_intelligence/tools.py +15 -10
  23. package/mcp_server/performance_engine/tools.py +6 -2
  24. package/mcp_server/preview_studio/tools.py +21 -15
  25. package/mcp_server/project_brain/tools.py +18 -10
  26. package/mcp_server/reference_engine/tools.py +7 -5
  27. package/mcp_server/runtime/capability_probe.py +10 -4
  28. package/mcp_server/runtime/tools.py +8 -2
  29. package/mcp_server/sample_engine/tools.py +27 -18
  30. package/mcp_server/semantic_moves/tools.py +5 -1
  31. package/mcp_server/server.py +10 -9
  32. package/mcp_server/services/motif_service.py +9 -3
  33. package/mcp_server/session_continuity/tools.py +7 -3
  34. package/mcp_server/session_continuity/tracker.py +9 -8
  35. package/mcp_server/song_brain/tools.py +17 -12
  36. package/mcp_server/splice_client/client.py +19 -6
  37. package/mcp_server/stuckness_detector/tools.py +8 -5
  38. package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
  39. package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
  40. package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
  41. package/mcp_server/tools/_agent_os_engine/models.py +132 -0
  42. package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
  43. package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
  44. package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
  45. package/mcp_server/tools/_composition_engine/__init__.py +67 -0
  46. package/mcp_server/tools/_composition_engine/analysis.py +174 -0
  47. package/mcp_server/tools/_composition_engine/critics.py +522 -0
  48. package/mcp_server/tools/_composition_engine/gestures.py +230 -0
  49. package/mcp_server/tools/_composition_engine/harmony.py +70 -0
  50. package/mcp_server/tools/_composition_engine/models.py +193 -0
  51. package/mcp_server/tools/_composition_engine/sections.py +371 -0
  52. package/mcp_server/tools/_perception_engine.py +18 -11
  53. package/mcp_server/tools/agent_os.py +23 -15
  54. package/mcp_server/tools/analyzer.py +7 -8
  55. package/mcp_server/tools/automation.py +6 -1
  56. package/mcp_server/tools/composition.py +25 -16
  57. package/mcp_server/tools/devices.py +10 -6
  58. package/mcp_server/tools/motif.py +7 -2
  59. package/mcp_server/tools/planner.py +6 -2
  60. package/mcp_server/tools/research.py +13 -10
  61. package/mcp_server/transition_engine/tools.py +6 -1
  62. package/mcp_server/translation_engine/tools.py +8 -6
  63. package/mcp_server/wonder_mode/engine.py +8 -3
  64. package/mcp_server/wonder_mode/tools.py +29 -21
  65. package/package.json +1 -1
  66. package/remote_script/LivePilot/__init__.py +1 -1
  67. package/mcp_server/tools/_agent_os_engine.py +0 -947
  68. package/mcp_server/tools/_composition_engine.py +0 -1530
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "livepilot",
12
12
  "description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, spectral perception, technique memory, sample intelligence, auto-composition, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.10.5",
13
+ "version": "1.10.6",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.10.5 — Ableton Live 12
1
+ # LivePilot v1.10.6 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.6 — Debuggability + Engine Modularization (April 17 2026)
4
+
5
+ Defensive-programming release. Zero behavior change for users; substantial
6
+ quality-of-life gains for developers and future debugging sessions.
7
+
8
+ ### Debuggability
9
+
10
+ - **Silent-exception sweep.** All 79 `except Exception: pass` sites across
11
+ `mcp_server/` now emit a `logger.debug("<func> failed: %s", exc)` breadcrumb
12
+ while preserving the original body (pass / return X / continue). Previously
13
+ invisible failures now leave a trail. Run with `LOG_LEVEL=DEBUG` to surface.
14
+ - **Credit-floor guard hardened.** `SpliceGRPCClient.download_sample()` now
15
+ enforces `CREDIT_HARD_FLOOR` defensively via `can_afford(1, budget=1)` before
16
+ the gRPC call. Tool-layer callers still gate upstream for UX; this closes
17
+ the hole if any future caller forgets. The docstring claimed this guard
18
+ existed — now the code matches.
19
+
20
+ ### Engine modularization
21
+
22
+ Two single-file engines (2,477 LOC combined) split into packages while keeping
23
+ the public surface identical. Callers that did `from . import X as engine` or
24
+ `from .X import Symbol` continue to work unchanged.
25
+
26
+ - **`mcp_server/tools/_composition_engine/`** — 6 sub-modules (models, sections,
27
+ critics, gestures, harmony, analysis) + facade. Was 1,530 LOC in one file;
28
+ now no sub-module exceeds 522 LOC.
29
+ - **`mcp_server/tools/_agent_os_engine/`** — 6 sub-modules (models, world_model,
30
+ critics, evaluation, techniques, taste) + facade. Was 947 LOC; now no
31
+ sub-module exceeds 207 LOC. `_clamp` promoted to models.py to resolve a
32
+ circular-dep risk between `evaluation` and `taste`.
33
+
34
+ ### Infra
35
+
36
+ - **CI matrix adds Python 3.11.** Ableton 12.3's embedded Python is 3.11 on
37
+ some platforms — catching drift pre-merge.
38
+ - **`livepilot.mcpb` removed from git tracking.** It was already excluded from
39
+ `.npmignore` and `.mcpbignore`; now it's no longer bloating git history every
40
+ release. Distribute via GitHub Releases.
41
+ - **`.git-backup-full/` deleted.** 3.4 MB worktree reclaim.
42
+
43
+ ### Docs
44
+
45
+ - **OSC address convention** documented in both `m4l_device/livepilot_bridge.js`
46
+ and `mcp_server/m4l_bridge.py` — the existing tolerant normalization at
47
+ `_parse_osc` now has a written contract.
48
+
49
+ ### Tests
50
+
51
+ 1756 pass, 1 skipped (macOS-only path test on non-darwin), 0 failures.
52
+
3
53
  ## 1.10.5 — Splice online catalog unblocked + Simpler sample-loading fixes (April 14 2026)
4
54
 
5
55
  The Splice integration was **never working online** in previous releases. The
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, sample intelligence, auto-composition, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains, device atlas, sample intelligence, auto-composition, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,6 +1,6 @@
1
- # LivePilot v1.10.5 — Architecture & Tool Reference
1
+ # LivePilot v1.10.6 — Architecture & Tool Reference
2
2
 
3
- Agentic production system for Ableton Live 12. 320 tools across 43 domains. Device atlas (1305 devices, 81 enriched), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O, Splice online catalog search and download via gRPC (v1.10.5 unblocked 19,690+ catalog hits previously inaccessible).
3
+ Agentic production system for Ableton Live 12. 320 tools across 43 domains. Device atlas (1305 devices, 81 enriched), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O, Splice online catalog search and download via gRPC (v1.10.6 unblocked 19,690+ catalog hits previously inaccessible).
4
4
 
5
5
  ## Architecture
6
6
 
@@ -104,7 +104,7 @@ Call `get_capability_state` at the start of any evaluation session. The response
104
104
  {
105
105
  "mode": "normal",
106
106
  "analyzer_connected": true,
107
- "bridge_version": "1.10.5",
107
+ "bridge_version": "1.10.6",
108
108
  "spectral_cache_age_ms": 1200,
109
109
  "flucoma_available": false,
110
110
  "session_connected": true
Binary file
@@ -14,6 +14,17 @@
14
14
  * - Chunk parameter reads: 4 per batch, 50ms delay
15
15
  * - Base64 encode all JSON responses
16
16
  * - Defer all LiveAPI operations via deferlow()
17
+ *
18
+ * OSC address convention:
19
+ * - OUTGOING (this file → server via udpsend): use WITH leading slash,
20
+ * e.g. outlet(0, "/response", encoded). The slash is part of the
21
+ * OSC address that udpsend packs into the packet.
22
+ * - INCOMING (server → this file via udpreceive): Max's udpreceive
23
+ * routes on the selector, so the server's address string must be
24
+ * "response"/"cmd" WITHOUT a leading slash (Max would otherwise
25
+ * treat the slash as a literal selector character). See
26
+ * mcp_server/m4l_bridge.py for the sending side and `_parse_osc`
27
+ * for the tolerant normalization.
17
28
  */
18
29
 
19
30
  autowatch = 1;
@@ -84,7 +95,7 @@ function anything() {
84
95
  function dispatch(cmd, args) {
85
96
  switch(cmd) {
86
97
  case "ping":
87
- send_response({"ok": true, "version": "1.10.5"});
98
+ send_response({"ok": true, "version": "1.10.6"});
88
99
  break;
89
100
  case "get_params":
90
101
  cmd_get_params(args);
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "livepilot",
4
4
  "display_name": "LivePilot — AI for Ableton Live",
5
- "version": "1.10.5",
5
+ "version": "1.10.6",
6
6
  "description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 320 AI-powered tools.",
7
7
  "long_description": "LivePilot is an agentic production system for Ableton Live 12. 320 tools across 43 domains — device atlas (1305 devices), sample intelligence (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, and 12 creative engines.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects via Device Atlas (1305 devices indexed)\n- Searches samples across Splice, Ableton browser, and filesystem\n- Plans compositions from text prompts with genre-aware layering\n- Slices samples with intent-based MIDI generation\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data (M4L bridge)\n- Diagnoses stuck sessions and generates creative rescue variants\n- Remembers your production style across sessions\n\n**How it works:**\nLivePilot installs a Remote Script in Ableton that communicates with the AI over a local TCP connection. Everything runs on your machine — no audio leaves your computer.",
8
8
  "author": {
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.10.5"
2
+ __version__ = "1.10.6"
@@ -45,6 +45,10 @@ from pathlib import Path
45
45
  from typing import Optional, Tuple
46
46
 
47
47
  from .layer_planner import LayerSpec
48
+ import logging
49
+
50
+ logger = logging.getLogger(__name__)
51
+
48
52
 
49
53
 
50
54
  _AUDIO_EXTENSIONS = (".wav", ".aif", ".aiff", ".flac")
@@ -216,9 +220,9 @@ async def _splice_resolve(
216
220
  query=layer.search_query,
217
221
  per_page=5,
218
222
  )
219
- except Exception:
223
+ except Exception as exc:
224
+ logger.debug("_splice_resolve failed: %s", exc)
220
225
  return None, "unresolved"
221
-
222
226
  samples = list(result.samples) if result and hasattr(result, "samples") else []
223
227
  if not samples:
224
228
  return None, "unresolved"
@@ -243,7 +247,8 @@ async def _splice_resolve(
243
247
  downloaded = await splice_client.download_sample(file_hash)
244
248
  if downloaded and Path(downloaded).exists():
245
249
  return downloaded, "splice_remote"
246
- except Exception:
250
+ except Exception as exc:
251
+ logger.debug("_splice_resolve failed: %s", exc)
247
252
  continue # try next hit
248
253
 
249
254
  return None, "unresolved"
@@ -286,7 +291,6 @@ async def resolve_sample_for_layer(
286
291
  lp = hit.get("file_path") if isinstance(hit, dict) else None
287
292
  if lp and Path(lp).exists():
288
293
  return lp, "browser"
289
- except Exception:
290
- pass
291
-
294
+ except Exception as exc:
295
+ logger.debug("resolve_sample_for_layer failed: %s", exc)
292
296
  return None, "unresolved"
@@ -14,6 +14,10 @@ from fastmcp import Context
14
14
  from ..server import mcp
15
15
  from .prompt_parser import parse_prompt
16
16
  from .engine import ComposerEngine
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
17
21
 
18
22
 
19
23
  # Singleton engine — stateless, safe to reuse
@@ -29,8 +33,8 @@ def _get_search_roots(ctx: Context) -> list:
29
33
  cfg = ctx.lifespan_context.get("sample_search_roots") if hasattr(ctx, "lifespan_context") else None
30
34
  if cfg:
31
35
  roots.extend(cfg)
32
- except Exception:
33
- pass
36
+ except Exception as exc:
37
+ logger.debug("_get_search_roots failed: %s", exc)
34
38
  return roots
35
39
 
36
40
 
@@ -52,7 +56,8 @@ async def _credit_safety_prelude(splice_client, max_credits: int) -> tuple[int,
52
56
  try:
53
57
  info = await splice_client.get_credits()
54
58
  credits_remaining = getattr(info, "credits", None)
55
- except Exception:
59
+ except Exception as exc:
60
+ logger.debug("_credit_safety_prelude failed: %s", exc)
56
61
  credits_remaining = None
57
62
 
58
63
  if credits_remaining is None:
@@ -153,9 +158,8 @@ async def augment_with_samples(
153
158
  info = ableton.send_command("get_session_info", {})
154
159
  session_context["tempo"] = info.get("tempo", 120)
155
160
  session_context["track_count"] = info.get("track_count", 0)
156
- except Exception:
157
- pass
158
-
161
+ except Exception as exc:
162
+ logger.debug("augment_with_samples failed: %s", exc)
159
163
  result = await _engine.augment(
160
164
  request=request,
161
165
  max_credits=max_credits,
@@ -11,6 +11,10 @@ import time
11
11
  import uuid
12
12
  from collections import deque
13
13
  from typing import Optional
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
14
18
 
15
19
  CONNECT_TIMEOUT = 5
16
20
  RECV_TIMEOUT = 20
@@ -153,7 +157,8 @@ class AbletonConnection:
153
157
  """Send a ping and return True if a pong is received."""
154
158
  try:
155
159
  return self.send_command("ping").get("pong") is True
156
- except Exception:
160
+ except Exception as exc:
161
+ logger.debug("ping failed: %s", exc)
157
162
  return False
158
163
 
159
164
  def send_command(self, command_type: str, params: Optional[dict] = None) -> dict:
@@ -17,7 +17,9 @@ from fastmcp import Context
17
17
  from ..server import mcp
18
18
  from . import engine
19
19
  from .models import CONSTRAINT_MODES
20
+ import logging
20
21
 
22
+ logger = logging.getLogger(__name__)
21
23
 
22
24
  # Module-level cache for active constraints and distillations
23
25
  _active_constraints: Optional[engine.ConstraintSet] = None
@@ -110,9 +112,8 @@ def distill_reference_principles(
110
112
  "groove_posture": tactics.get("groove_posture", {}),
111
113
  "harmonic_character": tactics.get("harmonic_character", ""),
112
114
  }
113
- except Exception:
114
- pass
115
-
115
+ except Exception as exc:
116
+ logger.debug("distill_reference_principles failed: %s", exc)
116
117
  # Try to get a reference profile from the reference engine
117
118
  if not reference_profile:
118
119
  try:
@@ -121,7 +122,8 @@ def distill_reference_principles(
121
122
  style_name or reference_description
122
123
  )
123
124
  reference_profile = profile.to_dict()
124
- except Exception:
125
+ except Exception as exc:
126
+ logger.debug("distill_reference_principles failed: %s", exc)
125
127
  # Fallback: build from description keywords
126
128
  reference_profile = _profile_from_description(reference_description)
127
129
 
@@ -204,9 +206,8 @@ def generate_constrained_variants(
204
206
  taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
205
207
  anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
206
208
  taste_graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store).to_dict()
207
- except Exception:
208
- pass
209
-
209
+ except Exception as exc:
210
+ logger.debug("generate_constrained_variants failed: %s", exc)
210
211
  ps = ps_engine.create_preview_set(
211
212
  request_text=f"[Constrained: {', '.join(active.constraints)}] {request_text}",
212
213
  kernel_id=kernel_id,
@@ -293,7 +294,6 @@ def generate_reference_inspired_variants(
293
294
  except Exception as e:
294
295
  return {"error": f"Failed to generate reference-inspired variants: {e}"}
295
296
 
296
-
297
297
  # ── Helpers ───────────────────────────────────────────────────────
298
298
 
299
299
 
@@ -305,6 +305,7 @@ def _get_song_brain_dict() -> dict:
305
305
  except Exception as _e:
306
306
  if __debug__:
307
307
  import sys
308
+
308
309
  print(f"LivePilot: SongBrain unavailable in creative_constraints: {_e}", file=sys.stderr)
309
310
  return {}
310
311
 
@@ -22,7 +22,9 @@ import time
22
22
  from typing import Optional
23
23
 
24
24
  from .models import ExperimentSet, ExperimentBranch, BranchSnapshot
25
+ import logging
25
26
 
27
+ logger = logging.getLogger(__name__)
26
28
 
27
29
  # ── In-memory experiment store ───────────────────────────────────────────────
28
30
 
@@ -34,9 +36,9 @@ def _gen_id(prefix: str, seed: str) -> str:
34
36
  h = hashlib.sha256(f"{prefix}:{seed}:{time.time()}".encode()).hexdigest()[:8]
35
37
  return f"{prefix}_{h}"
36
38
 
37
-
38
39
  # ── Create experiments ───────────────────────────────────────────────────────
39
40
 
41
+
40
42
  def create_experiment(
41
43
  request_text: str,
42
44
  move_ids: list[str],
@@ -83,9 +85,9 @@ def list_experiments() -> list[dict]:
83
85
  """List all experiment sets."""
84
86
  return [exp.to_dict() for exp in _EXPERIMENTS.values()]
85
87
 
86
-
87
88
  # ── Run experiments (requires Ableton connection) ────────────────────────────
88
89
 
90
+
89
91
  def run_branch(
90
92
  branch: ExperimentBranch,
91
93
  ableton, # AbletonConnection
@@ -143,7 +145,8 @@ def _run_branch_sync(branch, ableton, compiled_plan, capture_fn):
143
145
  for _ in range(steps_executed):
144
146
  try:
145
147
  ableton.send_command("undo", {})
146
- except Exception:
148
+ except Exception as exc:
149
+ logger.debug("_run_branch_sync failed: %s", exc)
147
150
  break
148
151
 
149
152
  branch.status = "evaluated" if steps_executed > 0 else "failed"
@@ -215,7 +218,8 @@ async def run_branch_async(
215
218
  for _ in range(steps_executed):
216
219
  try:
217
220
  ableton.send_command("undo", {})
218
- except Exception:
221
+ except Exception as exc:
222
+ logger.debug("run_branch_async failed: %s", exc)
219
223
  break
220
224
 
221
225
  # A branch is "evaluated" only if it actually applied at least one step.
@@ -244,9 +248,9 @@ def evaluate_branch(
244
248
  branch.score = result.get("score", 0.0)
245
249
  return branch
246
250
 
247
-
248
251
  # ── Commit / discard ─────────────────────────────────────────────────────────
249
252
 
253
+
250
254
  async def commit_branch_async(
251
255
  experiment: ExperimentSet,
252
256
  branch_id: str,
@@ -18,6 +18,9 @@ from fastmcp import Context
18
18
  from ..server import mcp
19
19
  from . import engine
20
20
  from .models import BranchSnapshot
21
+ import logging
22
+
23
+ logger = logging.getLogger(__name__)
21
24
 
22
25
 
23
26
  def _get_ableton(ctx: Context):
@@ -35,25 +38,22 @@ def _capture_snapshot(ctx: Context) -> BranchSnapshot:
35
38
  try:
36
39
  meters = ableton.send_command("get_track_meters", {"include_stereo": True})
37
40
  snapshot.track_meters = meters.get("tracks", [])
38
- except Exception:
39
- pass
40
-
41
+ except Exception as exc:
42
+ logger.debug("_capture_snapshot failed: %s", exc)
41
43
  # Spectral data (requires M4L analyzer)
42
44
  if spectral and spectral.is_connected:
43
45
  try:
44
46
  spec = spectral.get("spectrum")
45
47
  if spec:
46
48
  snapshot.spectrum = spec.get("value", {})
47
- except Exception:
48
- pass
49
-
49
+ except Exception as exc:
50
+ logger.debug("_capture_snapshot failed: %s", exc)
50
51
  try:
51
52
  rms_data = spectral.get("rms")
52
53
  if rms_data:
53
54
  snapshot.rms = rms_data.get("value")
54
- except Exception:
55
- pass
56
-
55
+ except Exception as exc:
56
+ logger.debug("_capture_snapshot failed: %s", exc)
57
57
  return snapshot
58
58
 
59
59
 
@@ -17,6 +17,9 @@ from fastmcp import Context
17
17
 
18
18
  from ..server import mcp
19
19
  from . import analyzer
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
20
23
 
21
24
 
22
25
  def _get_ableton(ctx: Context):
@@ -39,8 +42,8 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
39
42
  try:
40
43
  session = ableton.send_command("get_session_info", {})
41
44
  tracks = session.get("tracks", [])
42
- except Exception:
43
- pass
45
+ except Exception as exc:
46
+ logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
44
47
 
45
48
  try:
46
49
  matrix = ableton.send_command("get_scene_matrix")
@@ -50,15 +53,16 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
50
53
  zip(matrix.get("scenes", []), matrix.get("matrix", []))
51
54
  )
52
55
  ]
53
- except Exception:
54
- pass
56
+ except Exception as exc:
57
+ logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
55
58
 
56
59
  # Fetch motif data — via shared motif service
57
60
  try:
58
61
  from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
59
62
  notes_by_track = fetch_notes_from_ableton(ableton, tracks)
60
63
  motif_data = get_motif_data(notes_by_track)
61
- except Exception:
64
+ except Exception as exc:
65
+ logger.debug("_fetch_tracks_and_scenes failed: %s", exc)
62
66
  pass # Motif graph requires notes in clips; empty dict is valid fallback
63
67
 
64
68
  return tracks, scenes, motif_data
@@ -325,7 +329,6 @@ def suggest_payoff_repair(ctx: Context) -> dict:
325
329
  "repair_count": len(repairs),
326
330
  }
327
331
 
328
-
329
332
  # ── Helpers ───────────────────────────────────────────────────────
330
333
 
331
334
 
@@ -376,8 +379,8 @@ def _get_section_data(ableton) -> list[dict]:
376
379
  "density": round(density, 3),
377
380
  "has_drums": has_drums,
378
381
  })
379
- except Exception:
380
- pass
382
+ except Exception as exc:
383
+ logger.debug("_get_section_data failed: %s", exc)
381
384
 
382
385
  return sections
383
386
 
@@ -391,6 +394,7 @@ def _get_song_brain_dict() -> dict:
391
394
  except Exception as _e:
392
395
  if __debug__:
393
396
  import sys
397
+
394
398
  print(f"LivePilot: SongBrain unavailable in hook_hunter: {_e}", file=sys.stderr)
395
399
  return {}
396
400
 
@@ -421,7 +425,8 @@ def detect_hook_neglect(ctx: Context) -> dict:
421
425
 
422
426
  try:
423
427
  matrix = ableton.send_command("get_scene_matrix")
424
- except Exception:
428
+ except Exception as exc:
429
+ logger.debug("detect_hook_neglect failed: %s", exc)
425
430
  return {
426
431
  "neglected": False,
427
432
  "hook": hook.to_dict(),
@@ -7,6 +7,17 @@ the M4L device on the master track. Sends commands back for deep LOM access.
7
7
  Architecture:
8
8
  M4L → UDP:9880 → SpectralReceiver → SpectralCache → MCP tools
9
9
  MCP tools → M4LBridge → UDP:9881 → M4L device
10
+
11
+ OSC address convention:
12
+ - OUTGOING (this side → M4L): address string is sent WITHOUT a leading
13
+ slash because Max's `udpreceive` treats a literal '/' as part of the
14
+ selector. The JS side (livepilot_bridge.js) routes on bare selectors
15
+ like "cmd" / "ping".
16
+ - INCOMING (M4L → this side): the M4L side uses Max's `udpsend`, whose
17
+ outlet messages include the leading slash (e.g. "/response"). The
18
+ `_parse_osc` helper normalizes with `rest = "/" + rest.lstrip("/\\")`
19
+ so both forms are tolerated — keep that normalization; both sides
20
+ bend toward leniency but the outgoing convention here is slash-less.
10
21
  """
11
22
 
12
23
  from __future__ import annotations
@@ -17,6 +17,9 @@ from __future__ import annotations
17
17
  import time
18
18
  from dataclasses import dataclass, field
19
19
  from typing import Optional
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
20
23
 
21
24
 
22
25
  @dataclass
@@ -127,7 +130,8 @@ class TasteGraph:
127
130
  if self._persistent_store is not None:
128
131
  try:
129
132
  self._persistent_store.record_move_outcome(move_id, family, kept, score)
130
- except Exception:
133
+ except Exception as exc:
134
+ logger.debug("record_move_outcome failed: %s", exc)
131
135
  pass # persistence is best-effort
132
136
 
133
137
  def record_device_use(self, device_name: str, positive: bool = True) -> None:
@@ -249,9 +253,9 @@ class TasteGraph:
249
253
  "explanations": explanations,
250
254
  }
251
255
 
252
-
253
256
  # ── Builder ──────────────────────────────────────────────────────────────────
254
257
 
258
+
255
259
  def build_taste_graph(
256
260
  taste_store=None, # TasteMemoryStore
257
261
  anti_store=None, # AntiMemoryStore
@@ -299,6 +303,7 @@ def build_taste_graph(
299
303
  # Device affinities
300
304
  for dev_name, dev_data in persisted.get("device_affinities", {}).items():
301
305
  from .taste_graph import DeviceAffinity
306
+
302
307
  graph.device_affinities[dev_name] = DeviceAffinity(
303
308
  device_name=dev_name,
304
309
  affinity=dev_data.get("affinity", 0.0),
@@ -15,6 +15,10 @@ from ..evaluation.fabric import evaluate_sonic_move
15
15
  from .state_builder import build_mix_state
16
16
  from .critics import run_all_mix_critics
17
17
  from .planner import plan_mix_moves
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
18
22
 
19
23
 
20
24
  # ── Helpers ─────────────────────────────────────────────────────────
@@ -32,7 +36,8 @@ def _fetch_mix_data(ctx: Context) -> dict:
32
36
  try:
33
37
  info = ableton.send_command("get_track_info", {"track_index": i})
34
38
  track_infos.append(info)
35
- except Exception:
39
+ except Exception as exc:
40
+ logger.debug("_fetch_mix_data failed: %s", exc)
36
41
  continue
37
42
 
38
43
  # Get spectrum and RMS data directly from SpectralCache (not TCP)
@@ -51,8 +56,8 @@ def _fetch_mix_data(ctx: Context) -> dict:
51
56
  rms_snap = spectral.get("rms")
52
57
  if rms_snap:
53
58
  rms_data = rms_snap["value"] if isinstance(rms_snap["value"], dict) else rms_snap["value"]
54
- except Exception:
55
- pass
59
+ except Exception as exc:
60
+ logger.debug("_fetch_mix_data failed: %s", exc)
56
61
 
57
62
  return {
58
63
  "session_info": session_info,