livepilot 1.9.23 → 1.9.24

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 (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +2 -2
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +94 -0
  5. package/livepilot/.Codex-plugin/plugin.json +1 -1
  6. package/livepilot/.claude-plugin/plugin.json +1 -1
  7. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  8. package/livepilot/skills/livepilot-release/SKILL.md +14 -0
  9. package/livepilot.mcpb +0 -0
  10. package/manifest.json +1 -1
  11. package/mcp_server/__init__.py +1 -1
  12. package/mcp_server/hook_hunter/analyzer.py +23 -0
  13. package/mcp_server/hook_hunter/models.py +1 -0
  14. package/mcp_server/hook_hunter/tools.py +4 -2
  15. package/mcp_server/memory/taste_graph.py +68 -1
  16. package/mcp_server/memory/tools.py +15 -4
  17. package/mcp_server/musical_intelligence/detectors.py +14 -1
  18. package/mcp_server/musical_intelligence/tools.py +11 -8
  19. package/mcp_server/persistence/__init__.py +1 -0
  20. package/mcp_server/persistence/base_store.py +82 -0
  21. package/mcp_server/persistence/project_store.py +106 -0
  22. package/mcp_server/persistence/taste_store.py +122 -0
  23. package/mcp_server/preview_studio/models.py +1 -0
  24. package/mcp_server/preview_studio/tools.py +56 -13
  25. package/mcp_server/runtime/capability.py +66 -0
  26. package/mcp_server/runtime/capability_probe.py +118 -0
  27. package/mcp_server/runtime/execution_router.py +139 -0
  28. package/mcp_server/runtime/remote_commands.py +82 -0
  29. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  30. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  31. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  32. package/mcp_server/semantic_moves/tools.py +18 -17
  33. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  34. package/mcp_server/services/__init__.py +1 -0
  35. package/mcp_server/services/motif_service.py +67 -0
  36. package/mcp_server/session_continuity/tracker.py +29 -1
  37. package/mcp_server/song_brain/builder.py +28 -1
  38. package/mcp_server/song_brain/models.py +4 -0
  39. package/mcp_server/song_brain/tools.py +20 -2
  40. package/mcp_server/wonder_mode/tools.py +6 -1
  41. package/package.json +1 -1
  42. package/remote_script/LivePilot/__init__.py +1 -1
  43. package/scripts/sync_metadata.py +132 -0
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "livepilot",
12
12
  "description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.9.23",
13
+ "version": "1.9.24",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.18 — Ableton Live 12
1
+ # LivePilot v1.9.24 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
@@ -43,4 +43,4 @@ When modifying .amxd attributes that Max editor won't persist (e.g., `openinpres
43
43
  4. Structure: 24-byte `ampf` header + `ptch` chunk + `mx@c` header + JSON patcher + frozen deps
44
44
 
45
45
  ## Tool Count
46
- Currently 257 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
46
+ Currently 293 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, livepilot/.claude-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.24 — Stability & Intelligence Upgrade (April 2026)
4
+
5
+ ### Truth and Boundaries (Wave 1)
6
+ - **feat(runtime):** Capability contract — every advanced tool reports `full/fallback/analytical_only/unavailable` with confidence scores
7
+ - **feat(runtime):** Command boundary audit — CI catches any `send_command()` targeting a non-existent Remote Script command
8
+ - **fix(song_brain):** `get_motif_graph` now uses pure-Python engine instead of invalid TCP call
9
+ - **fix(hook_hunter):** Same motif routing fix
10
+ - **fix(musical_intelligence):** Same motif routing fix + `analyze_phrase_arc` now calls perception engine directly
11
+ - **fix(memory):** `record_positive_preference` actually updates taste dimensions (was a silent no-op due to key mismatch)
12
+ - **fix(metadata):** AGENTS.md synced to v1.9.23/293 tools, test docstring corrected
13
+
14
+ ### Unified Execution Layer (Wave 2)
15
+ - **feat(runtime):** Execution router — classifies steps as `remote_command/bridge_command/mcp_tool/unknown`, dispatches correctly
16
+ - **feat(semantic_moves):** `apply_semantic_move` explore mode uses execution router
17
+ - **feat(preview_studio):** `render_preview_variant` uses execution router
18
+
19
+ ### Persistent Memory (Waves 2-3)
20
+ - **feat(persistence):** Base persistent JSON store (atomic write, corruption recovery, thread-safe)
21
+ - **feat(persistence):** Taste store at `~/.livepilot/taste.json` — move outcomes, novelty band, device affinity, anti-preferences survive restart
22
+ - **feat(persistence):** Project store at `~/.livepilot/projects/<hash>/state.json` — threads, turns, Wonder outcomes per song
23
+ - **feat(memory):** TasteGraph.record_move_outcome writes to persistent backing
24
+ - **feat(session_continuity):** tracker flushes threads and turns to project store on write
25
+
26
+ ### Move Annotations (Wave 3)
27
+ - **feat(semantic_moves):** All 20 moves annotated with explicit `backend` per compile_plan step
28
+ - **test:** Static audit verifies all annotations match the execution router classifier
29
+
30
+ ### Intelligence Upgrade (Waves 3-4)
31
+ - **feat(services):** Shared motif service — one entry point consumed by SongBrain, HookHunter, musical_intelligence
32
+ - **feat(song_brain):** Evidence-weighted identity confidence (motif=0.4, composition=0.2, roles=0.15, scenes=0.15, moves=0.1)
33
+ - **feat(song_brain):** `evidence_breakdown` field shows per-source contributions
34
+ - **feat(hook_hunter):** Hooks carry `evidence_sources` (motif_recurrence, track_name, clip_reuse)
35
+ - **feat(hook_hunter):** Section-placement analysis boosts hooks recurring across sections
36
+ - **feat(detectors):** Motif appearing in >60% of sections triggers fatigue signal
37
+
38
+ ### Preview and Doctor (Wave 4)
39
+ - **feat(preview_studio):** Three explicit preview modes: `audible_preview` (M4L+spectrum), `metadata_only_preview`, `analytical_preview`
40
+ - **feat(preview_studio):** `bars` parameter used for audible preview playback duration
41
+ - **feat(preview_studio):** `preview_mode` field in response — no ambiguity about what was measured
42
+ - **feat(runtime):** Capability probe — 6-area runtime detection (Ableton, Remote Script, M4L, numpy, persistence, tier)
43
+
44
+ ### Release Infrastructure (Wave 5)
45
+ - **feat(scripts):** `sync_metadata.py` — single source of truth for version and tool count, CI-checkable
46
+ - **docs:** README Intelligence Layer section with all 12 engines described
47
+ - **docs:** Manual index rewritten with three-layer architecture and 39-domain map
48
+
3
49
  ## 1.9.23-wonder-v1.5 — Wonder Mode V1.5: Stuck-Rescue Workflow (April 2026)
4
50
 
5
51
  ### Wonder Mode Redesign (292->293 tools)
package/README.md CHANGED
@@ -77,6 +77,77 @@ All three feed into 293 deterministic tools that execute on Ableton's main threa
77
77
 
78
78
  ---
79
79
 
80
+ ## The Intelligence Layer
81
+
82
+ Most MCP servers are tool collections — they execute commands. LivePilot is an **agentic production system** — it understands what a song is becoming, diagnoses when a session is stuck, generates real creative options, learns from your decisions, and tracks its own impact.
83
+
84
+ This is the V2 intelligence layer: 12 engines that sit on top of the 293 tools and give the AI musical judgment, not just musical execution.
85
+
86
+ ### SongBrain — What the Song Is
87
+
88
+ SongBrain builds a real-time model of the current session: what the defining idea is (identity core), what elements must not be casually damaged (sacred elements), what each section is trying to do emotionally (section purposes), and where the energy arc is heading. It answers the question every producer asks: *"What is this track?"*
89
+
90
+ It detects when the song's identity is drifting — when recent edits are pulling the track away from what made it work. When identity confidence is high, the system makes bolder suggestions. When it's fragile, it protects what's there.
91
+
92
+ ### Taste Graph — What You Like
93
+
94
+ The Taste Graph learns your production preferences across sessions. Not just "prefers reverb" — it tracks which move families you keep vs. undo (mix moves? arrangement moves?), which devices you gravitate toward, how experimental you want suggestions to be (your novelty band), and which dimensions you actively avoid.
95
+
96
+ Every time you accept or reject a suggestion, the graph updates. Over time, it personalizes which creative options are offered and how they're ranked. Two producers using the same tools get different recommendations.
97
+
98
+ ### Semantic Moves — Musical Actions, Not Parameters
99
+
100
+ A semantic move is a high-level musical intent — "add contrast," "tighten the low end," "build tension toward the chorus" — that compiles into a specific sequence of tool calls. The system has 20 moves across 4 families (mix, arrangement, transition, sound design), each with an executable plan.
101
+
102
+ Moves carry risk levels, target dimensions, and protection thresholds. "Add a filter sweep build" targets energy and tension while protecting clarity. The AI doesn't just know what to do — it knows what it's risking.
103
+
104
+ ### Wonder Mode — Stuck-Rescue Workflow
105
+
106
+ When a session is stuck — too many undos, polishing the same loop, no structural progress — Wonder Mode activates. It's not "surprise me." It's a structured diagnosis-and-rescue workflow:
107
+
108
+ 1. **Diagnose** — Why is the session stuck? Repeated undos? Overpolished loop? Missing contrast? Identity unclear? The stuckness detector analyzes the action history and classifies the problem.
109
+
110
+ 2. **Generate** — Based on the diagnosis, Wonder searches for semantic moves that address the specific problem. It enforces real distinctness — each variant must differ by move family or execution approach. If only one real option exists, it says so honestly instead of relabeling the same idea three times.
111
+
112
+ 3. **Preview** — Each executable variant can be applied, captured, and undone using Ableton's undo system. You hear what each option would actually sound like before committing.
113
+
114
+ 4. **Commit or Reject** — Choose one, and the system records it into taste and session continuity. Reject all, and the creative thread stays open for another attempt. No fake outcomes are recorded.
115
+
116
+ ### Creative Engines
117
+
118
+ Six specialized engines handle different aspects of production intelligence:
119
+
120
+ | Engine | What it does |
121
+ |--------|-------------|
122
+ | **Mix Engine** | Critic-driven mix analysis. Identifies masking, headroom issues, stereo problems. Plans corrective moves with before/after evaluation. |
123
+ | **Sound Design Engine** | Analyzes patches for static timbre, missing modulation, weak transients. Suggests parameter moves and evaluates the result. |
124
+ | **Transition Engine** | Classifies transition types (drop, build, breakdown). Scores transition quality and plans improvements using archetypes. |
125
+ | **Composition Engine** | Analyzes song structure, detects motifs, infers section purposes, scores emotional arcs. Plans arrangement moves. |
126
+ | **Performance Engine** | Safety-constrained suggestions for live performance. Knows which moves are safe during playback and which risk audio dropouts. |
127
+ | **Reference Engine** | Distills principles from reference tracks. Maps those principles to your current session as concrete, actionable moves. |
128
+
129
+ ### Hook Hunter — Finding What Matters
130
+
131
+ The Hook Hunter identifies the most salient musical idea in a session — the element listeners would remember. It ranks candidates by rhythmic distinctiveness, melodic contour, and repetition. Then it tracks whether hooks are being developed, neglected, or undermined by arrangement choices.
132
+
133
+ When the hook is strong but underused, it flags it. When a transition fails to deliver the expected payoff, it diagnoses why.
134
+
135
+ ### Session Continuity — The Story of Your Session
136
+
137
+ Session Continuity tracks what happened, what changed, and what's still unresolved. It maintains creative threads (open questions like "the chorus needs more lift") and records turn resolutions (what you tried, whether you kept it, how it affected identity).
138
+
139
+ When you return to a project, the session story tells the AI: *"Last time, you were working on making the bridge darker. You tried three approaches and kept the filter sweep. The chorus lift thread is still open."*
140
+
141
+ ### Evaluation Loop — Verify Before Claiming Success
142
+
143
+ Every creative engine follows the same discipline: **measure before, act, measure after, compare**. The evaluation system captures session state snapshots, runs the change, captures again, and scores the difference. If the change made things worse — more masking, lost headroom, identity drift — the system flags it before you move on.
144
+
145
+ This closes the gap between "the AI did something" and "the AI did something that actually helped."
146
+
147
+ <br>
148
+
149
+ ---
150
+
80
151
  ## Tools
81
152
 
82
153
  293 tools across 39 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
@@ -322,6 +393,29 @@ read_audio_metadata Format, duration, sample rate, tags
322
393
 
323
394
  <br>
324
395
 
396
+ ### Agentic Intelligence — 83 tools
397
+
398
+ The V2 intelligence layer. These tools don't just execute commands — they analyze, diagnose, plan, evaluate, and learn.
399
+
400
+ | Domain | # | What it does |
401
+ |--------|:-:|-------------|
402
+ | Agent OS | 8 | session kernel, action ledger, capability state, routing, turn budget |
403
+ | Composition | 9 | section analysis, motif detection, emotional arc, form planning, section transforms |
404
+ | Evaluation | 1 | before/after evaluation with structured scoring |
405
+ | Mix Engine | 6 | critic-driven mix analysis, issue detection, move planning, masking reports |
406
+ | Sound Design | 5 | patch analysis, modulation planning, timbre scoring |
407
+ | Transition Engine | 5 | transition classification, scoring, archetype-based planning |
408
+ | Reference Engine | 5 | reference profiling, principle distillation, gap analysis, move mapping |
409
+ | Translation Engine | 3 | cross-domain translation, issue detection |
410
+ | Performance Engine | 5 | safety-constrained suggestions, safe move lists, scene handoff planning |
411
+ | Song Brain | 4 | identity inference, sacred element detection, drift monitoring, section purposes |
412
+ | Hook Hunter | 9 | hook detection, salience scoring, development strategies, neglect detection, phrase impact |
413
+ | Stuckness Detector | 3 | momentum analysis, rescue classification, structured rescue workflows |
414
+ | Wonder Mode | 3 | diagnosis-driven variant generation, taste-aware ranking, session discard |
415
+ | Session Continuity | 7 | creative threads, turn resolution, taste vs identity ranking, session story |
416
+ | Creative Constraints | 5 | constraint activation, reference-inspired variants, constrained generation |
417
+ | Preview Studio | 5 | variant creation, preview rendering, comparison, commit, discard |
418
+
325
419
  > **[View all 293 tools →](docs/manual/tool-catalog.md)**
326
420
 
327
421
  <br>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.23",
3
+ "version": "1.9.24",
4
4
  "description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, 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.9.23",
3
+ "version": "1.9.24",
4
4
  "description": "Agentic production system for Ableton Live 12 — 293 tools, 39 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.23 — Architecture & Tool Reference
1
+ # LivePilot v1.9.24 — Architecture & Tool Reference
2
2
 
3
3
  Agentic production system for Ableton Live 12. 293 tools across 39 domains. Device atlas (280+ devices), 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.
4
4
 
@@ -105,6 +105,20 @@ Current: **39 domains**: transport, tracks, clips, notes, devices, scenes, mixin
105
105
  - [ ] Remote script version matches MCP server version
106
106
  - [ ] All tests pass: `python3 -m pytest tests/ -v`
107
107
 
108
+ ## 11. Automated Checks
109
+
110
+ - [ ] `python scripts/sync_metadata.py --check` — all metadata in sync
111
+ - [ ] `python -m pytest tests/test_command_boundary_audit.py` — no invalid TCP targets
112
+ - [ ] `python -m pytest tests/test_move_annotations.py` — all moves annotated
113
+ - [ ] `python -m pytest tests/test_capability.py` — capability contract works
114
+ - [ ] `python -m pytest tests/test_capability_probe.py` — doctor probe works
115
+
116
+ ## 12. Release Smoke Board
117
+
118
+ - [ ] Run through `docs/manual/release-smoke-board.md` scenarios against real Ableton session
119
+ - [ ] All preview modes correctly labeled (audible/metadata/analytical)
120
+ - [ ] Persistence survives server restart
121
+
108
122
  ## Quick Verify Command
109
123
 
110
124
  ```bash
package/livepilot.mcpb CHANGED
Binary file
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.9.23",
5
+ "version": "1.9.24",
6
6
  "description": "Agentic production system for Ableton Live 12. Make beats, mix tracks, design sounds, and arrange songs with 293 AI-powered tools.",
7
7
  "long_description": "LivePilot is an AI production assistant that connects directly to Ableton Live 12. It can create drum patterns, program basslines, write chord progressions, design sounds, mix your tracks, analyze your audio, and arrange full songs — all through natural language.\n\n**What it does:**\n- Creates MIDI clips with notes, chords, and rhythms\n- Loads instruments and effects from Ableton's browser\n- Shapes sounds by adjusting device parameters\n- Mixes with volume, panning, sends, and automation\n- Analyzes your mix with real-time spectral data\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.9.23"
2
+ __version__ = "1.9.24"
@@ -82,9 +82,32 @@ def find_hook_candidates(
82
82
  development_potential=0.5,
83
83
  ))
84
84
 
85
+ # 4. Section-placement analysis: boost hooks that appear in payoff sections
86
+ payoff_sections = {
87
+ s.get("label", "").lower()
88
+ for s in (composition.get("sections", []) if composition else [])
89
+ if s.get("is_payoff")
90
+ } or {"chorus", "drop", "hook"}
91
+
92
+ for c in candidates:
93
+ # Check if hook is present in payoff sections (via motif locations)
94
+ if c.hook_type == "melodic" and motif_data:
95
+ for motif in motif_data.get("motifs", []):
96
+ if motif.get("name", "") in c.hook_id:
97
+ # Motif with high recurrence across sections = stronger hook
98
+ c.memorability = min(1.0, c.memorability + motif.get("recurrence", 0) * 0.2)
99
+
85
100
  # Score all candidates
86
101
  for c in candidates:
87
102
  c.salience = _compute_salience(c)
103
+ # Add evidence sources
104
+ c.evidence_sources = []
105
+ if "motif_" in c.hook_id:
106
+ c.evidence_sources.append("motif_recurrence")
107
+ if "track_" in c.hook_id:
108
+ c.evidence_sources.append("track_name")
109
+ if "groove" in c.hook_id:
110
+ c.evidence_sources.append("clip_reuse")
88
111
 
89
112
  # Sort by salience
90
113
  candidates.sort(key=lambda c: c.salience, reverse=True)
@@ -19,6 +19,7 @@ class HookCandidate:
19
19
  contrast_potential: float = 0.0 # 0-1 how well it stands out
20
20
  development_potential: float = 0.0 # 0-1 how much room to develop
21
21
  salience: float = 0.0 # composite score
22
+ evidence_sources: list[str] = field(default_factory=list) # what data informed this
22
23
 
23
24
  def to_dict(self) -> dict:
24
25
  return asdict(self)
@@ -53,9 +53,11 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
53
53
  except Exception:
54
54
  pass
55
55
 
56
- # Fetch motif data from the motif engine for salience-based hook discovery
56
+ # Fetch motif data via shared motif service
57
57
  try:
58
- motif_data = ableton.send_command("get_motif_graph")
58
+ from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
59
+ notes_by_track = fetch_notes_from_ableton(ableton, tracks)
60
+ motif_data = get_motif_data(notes_by_track)
59
61
  except Exception:
60
62
  pass # Motif graph requires notes in clips; empty dict is valid fallback
61
63
 
@@ -99,6 +99,9 @@ class TasteGraph:
99
99
 
100
100
  # ── Update methods ───────────────────────────────────────────────
101
101
 
102
+ # Persistent store reference (set by build_taste_graph when available)
103
+ _persistent_store: object = None
104
+
102
105
  def record_move_outcome(
103
106
  self, move_id: str, family: str, kept: bool, score: float = 0.0
104
107
  ) -> None:
@@ -120,6 +123,13 @@ class TasteGraph:
120
123
  self.evidence_count += 1
121
124
  self.last_updated_ms = now
122
125
 
126
+ # Write-back to persistent store
127
+ if self._persistent_store is not None:
128
+ try:
129
+ self._persistent_store.record_move_outcome(move_id, family, kept, score)
130
+ except Exception:
131
+ pass # persistence is best-effort
132
+
123
133
  def record_device_use(self, device_name: str, positive: bool = True) -> None:
124
134
  """Update device affinity from usage."""
125
135
  now = int(time.time() * 1000)
@@ -245,10 +255,17 @@ class TasteGraph:
245
255
  def build_taste_graph(
246
256
  taste_store=None, # TasteMemoryStore
247
257
  anti_store=None, # AntiMemoryStore
258
+ persistent_store=None, # PersistentTasteStore (optional)
248
259
  ) -> TasteGraph:
249
- """Build a TasteGraph from existing memory stores."""
260
+ """Build a TasteGraph from existing memory stores.
261
+
262
+ When persistent_store is provided, hydrates move_family_scores,
263
+ device_affinities, and novelty_band from disk — these survive
264
+ server restart.
265
+ """
250
266
  graph = TasteGraph()
251
267
 
268
+ # Session-scoped dimensions (in-memory)
252
269
  if taste_store:
253
270
  for dim in taste_store.get_taste_dimensions():
254
271
  if dim.evidence_count > 0:
@@ -258,4 +275,54 @@ def build_taste_graph(
258
275
  for pref in anti_store.get_anti_preferences():
259
276
  graph.dimension_avoidances[pref.dimension] = pref.direction
260
277
 
278
+ # Persistent state (from disk)
279
+ if persistent_store is not None:
280
+ persisted = persistent_store.get_all()
281
+
282
+ # Move family scores
283
+ for move_id, outcome in persisted.get("move_outcomes", {}).items():
284
+ family = outcome.get("family", "")
285
+ if family and family not in graph.move_family_scores:
286
+ from .taste_graph import MoveFamilyScore
287
+ graph.move_family_scores[family] = MoveFamilyScore(family=family)
288
+ if family:
289
+ fam = graph.move_family_scores[family]
290
+ fam.kept_count += outcome.get("kept_count", 0)
291
+ fam.undone_count += outcome.get("undone_count", 0)
292
+ total = fam.kept_count + fam.undone_count
293
+ if total > 0:
294
+ fam.score = round((fam.kept_count - fam.undone_count) / total, 3)
295
+
296
+ # Novelty band
297
+ graph.novelty_band = persisted.get("novelty_band", 0.5)
298
+
299
+ # Device affinities
300
+ for dev_name, dev_data in persisted.get("device_affinities", {}).items():
301
+ from .taste_graph import DeviceAffinity
302
+ graph.device_affinities[dev_name] = DeviceAffinity(
303
+ device_name=dev_name,
304
+ affinity=dev_data.get("affinity", 0.0),
305
+ use_count=dev_data.get("use_count", 0),
306
+ )
307
+
308
+ # Evidence count
309
+ graph.evidence_count = max(
310
+ graph.evidence_count, persisted.get("evidence_count", 0)
311
+ )
312
+
313
+ # Dimension weights from persistent store (merged, session takes precedence)
314
+ for dim, val in persisted.get("dimension_weights", {}).items():
315
+ if dim not in graph.dimension_weights:
316
+ graph.dimension_weights[dim] = val
317
+
318
+ # Anti-preferences from persistent store
319
+ for anti in persisted.get("anti_preferences", []):
320
+ dim = anti.get("dimension", "")
321
+ direction = anti.get("direction", "")
322
+ if dim and dim not in graph.dimension_avoidances:
323
+ graph.dimension_avoidances[dim] = direction
324
+
325
+ # Attach persistent store for write-back
326
+ graph._persistent_store = persistent_store
327
+
261
328
  return graph
@@ -189,12 +189,23 @@ def record_positive_preference(
189
189
  not just what they dislike.
190
190
  """
191
191
  taste_store = _get_taste_memory(ctx)
192
- # Map to outcome signal
193
- signal = f"{dimension}_{direction}_kept"
194
- taste_store.update_from_outcome({"signal": signal})
192
+ # Find matching outcome signals for this dimension+direction
193
+ from ..memory.taste_memory import _OUTCOME_SIGNALS
194
+ matching_signals = []
195
+ dim_signals = _OUTCOME_SIGNALS.get(dimension, {})
196
+ for sig_name, adjustment in dim_signals.items():
197
+ # "increase" preference → match positive-adjustment signals (kept)
198
+ # "decrease" preference → match negative-adjustment signals (undone/less)
199
+ if direction == "increase" and adjustment > 0:
200
+ matching_signals.append(sig_name)
201
+ elif direction == "decrease" and adjustment < 0:
202
+ matching_signals.append(sig_name)
203
+ if matching_signals:
204
+ taste_store.update_from_outcome({"signals": matching_signals})
195
205
  return {
196
- "recorded": True,
206
+ "recorded": bool(matching_signals),
197
207
  "dimension": dimension,
198
208
  "direction": direction,
209
+ "signals_matched": matching_signals,
199
210
  "evidence": evidence,
200
211
  }
@@ -96,9 +96,22 @@ def detect_repetition_fatigue(
96
96
  # 3. Motif fatigue from motif_graph
97
97
  if motif_graph:
98
98
  motifs = motif_graph.get("motifs", [])
99
+ num_sections = max(1, len(scenes))
99
100
  for motif in motifs:
100
101
  fatigue_risk = motif.get("fatigue_risk", 0)
101
- if fatigue_risk > 0.6:
102
+ recurrence = motif.get("recurrence", 0)
103
+
104
+ # Motif appearing in >60% of sections = fatigue signal
105
+ if recurrence > 0.6 and num_sections >= 3:
106
+ adjusted_fatigue = max(fatigue_risk, recurrence * 0.8)
107
+ report.issues.append({
108
+ "type": "motif_overuse",
109
+ "severity": round(adjusted_fatigue, 3),
110
+ "detail": f"Motif {motif.get('name', motif.get('motif_id', '?'))} appears in {recurrence:.0%} of sections",
111
+ "motif_id": motif.get("motif_id", motif.get("name", "")),
112
+ "evidence": "motif_recurrence",
113
+ })
114
+ elif fatigue_risk > 0.6:
102
115
  report.issues.append({
103
116
  "type": "motif_overuse",
104
117
  "severity": fatigue_risk,
@@ -45,10 +45,14 @@ def detect_repetition_fatigue(ctx: Context) -> dict:
45
45
  "clips": row,
46
46
  })
47
47
 
48
- # Try to get motif graph for deeper analysis
48
+ # Motif data via shared motif service
49
49
  motif_graph = None
50
50
  try:
51
- motif_graph = ableton.send_command("get_motif_graph")
51
+ from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
52
+ session_info = ableton.send_command("get_session_info", {})
53
+ track_list = session_info.get("tracks", [])
54
+ notes_by_track = fetch_notes_from_ableton(ableton, track_list)
55
+ motif_graph = get_motif_data(notes_by_track)
52
56
  except Exception:
53
57
  pass
54
58
 
@@ -171,17 +175,16 @@ def analyze_phrase_arc(
171
175
  loudness_data = None
172
176
  spectrum_data = None
173
177
 
178
+ # Direct Python calls to perception engine — not TCP
174
179
  try:
175
- loudness_data = ableton.send_command("analyze_loudness_offline", {
176
- "file_path": file_path, "detail": "full",
177
- })
180
+ from ..tools._perception_engine import compute_loudness
181
+ loudness_data = compute_loudness(file_path, detail="full")
178
182
  except Exception:
179
183
  pass
180
184
 
181
185
  try:
182
- spectrum_data = ableton.send_command("analyze_spectrum_offline_internal", {
183
- "file_path": file_path,
184
- })
186
+ from ..tools._perception_engine import compute_spectral
187
+ spectrum_data = compute_spectral(file_path)
185
188
  except Exception:
186
189
  pass
187
190
 
@@ -0,0 +1 @@
1
+ """Persistent storage for LivePilot state that survives server restart."""
@@ -0,0 +1,82 @@
1
+ """Persistent JSON store with atomic writes and corruption recovery.
2
+
3
+ Follows the TechniqueStore pattern: lazy init, atomic tmp+rename,
4
+ fsync to disk, corruption recovery via .corrupt rename.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import threading
12
+ from pathlib import Path
13
+
14
+
15
+ class PersistentJsonStore:
16
+ """Thread-safe, crash-safe JSON file store."""
17
+
18
+ def __init__(self, path: Path):
19
+ self._path = Path(path)
20
+ self._lock = threading.RLock()
21
+
22
+ @property
23
+ def path(self) -> Path:
24
+ return self._path
25
+
26
+ def read(self) -> dict:
27
+ """Read the store. Returns {} if missing or corrupt."""
28
+ with self._lock:
29
+ if not self._path.exists():
30
+ return {}
31
+ try:
32
+ return json.loads(self._path.read_text(encoding="utf-8"))
33
+ except (json.JSONDecodeError, OSError):
34
+ corrupt = self._path.with_suffix(self._path.suffix + ".corrupt")
35
+ try:
36
+ self._path.rename(corrupt)
37
+ except OSError:
38
+ pass
39
+ return {}
40
+
41
+ def write(self, data: dict) -> None:
42
+ """Atomically write data to disk."""
43
+ with self._lock:
44
+ self._path.parent.mkdir(parents=True, exist_ok=True)
45
+ tmp = self._path.with_suffix(".tmp")
46
+ try:
47
+ with open(tmp, "w", encoding="utf-8") as f:
48
+ json.dump(data, f, indent=2, default=str)
49
+ f.flush()
50
+ os.fsync(f.fileno())
51
+ os.replace(str(tmp), str(self._path))
52
+ except OSError:
53
+ try:
54
+ tmp.unlink(missing_ok=True)
55
+ except OSError:
56
+ pass
57
+ raise
58
+
59
+ def update(self, updater) -> dict:
60
+ """Read-modify-write atomically. updater(data) -> modified data."""
61
+ with self._lock:
62
+ data = self._read_unlocked()
63
+ data = updater(data)
64
+ self._write_unlocked(data)
65
+ return data
66
+
67
+ def _read_unlocked(self) -> dict:
68
+ if not self._path.exists():
69
+ return {}
70
+ try:
71
+ return json.loads(self._path.read_text(encoding="utf-8"))
72
+ except (json.JSONDecodeError, OSError):
73
+ return {}
74
+
75
+ def _write_unlocked(self, data: dict) -> None:
76
+ self._path.parent.mkdir(parents=True, exist_ok=True)
77
+ tmp = self._path.with_suffix(".tmp")
78
+ with open(tmp, "w", encoding="utf-8") as f:
79
+ json.dump(data, f, indent=2, default=str)
80
+ f.flush()
81
+ os.fsync(f.fileno())
82
+ os.replace(str(tmp), str(self._path))