Character-stack unification — migration plan
Why
NAO 2026-05-23 directive: unify VRM + movement + speech + lipsync across live / persona / world / social so they snap together like LEGO. The first concrete cleanup is the parallel-impl problem between openalice-world and openalice-persona: both ship a character-stack monorepo, the world version scopes its packages as @openalice/* while persona scopes them as @openalicelabs/persona-*, and on file content they are ≥ 99% duplicates. Single canonical home = one source of truth, one place to fix bugs, one runtime under audit.
This note is the work-plan for daylight. Tonight is audit only; the merge happens with proper regression testing.
Today's ground truth
| Package pair (world ↔ persona) | World files | Persona files | Identical | Divergent | World-only |
|---|---|---|---|---|---|
| avatar ↔ core | 2 | 23 | 0 | 0 | 2 (shim) |
| engine ↔ engine | 12 + 3 tests | 8 | 1 | 7 | 3 tests |
| multiplayer ↔ multiplayer | 9 + 3 tests | 5 | 3 | 2 | 1 + 3 tests |
| voice ↔ voice | 3 | 4 | 1 | 2 | 0 |
| world ↔ world | 14 | 15 | 10 | 4 | 0 |
Total impact to apps/web: ~48 @openalice/* import statements across ~20 files in openalice-world/apps/web/src/**. ~18 source files have content divergence and need careful merge (not auto sed-rename).
Divergent files needing careful merge
engine (7 divergent)
audio/soundManager.ts— world addsdisposeSoundManager()+ null-checks on singleton reset (SPA-navigation memory safety). Persona's version lacks these; world's is the better baseline.audio/proceduralAudio.ts— similar enhancement-style drift.terrain/generator.ts,particles/engine.ts,quality/adaptive.ts,geometry/procedural.ts,shaders/grass.ts— 10–50 line deltas each, likely independent tuning. Need side-by-side diff before merge.
multiplayer (2 divergent + 1 world-only)
index.ts— world exportsWorldAgent+ types; persona doesn't.spatialAudio.ts,useInterpolation.ts,usePrediction.ts,useWorldSync.ts— likely diverged between player-centric (world) and agent-centric (persona) flows.worldAgent.ts(219 lines, world-only) — FPS player state + world sync protocol. Either promote to persona or keep world-only.
voice (2 divergent + 1 persona-only)
voiceChat.ts— world's version importsuseVoiceClientfrom@openalicelabs/voice-clientand adds game-audio ducking via@openalice/engine. Persona's version is older + simpler. World's is the better baseline.voiceInput.ts— flag for diff review.clientLog.ts(persona-only, 19 lines) — small logging util.
world (4 divergent + 2 world-uses)
PlayerAvatar.tsx(218 → 239 lines) — world has ~21 lines of player-specific rendering on top of persona's base.AnimePostProcessing.tsx,DynamicSky.tsx,IslandScene.tsx,MobileControls.tsx,RemotePlayer.tsx,ScreenShareBillboard.tsx,VRControls.tsx,WorldHUD.tsx,WorldParticles.tsx— incremental drifts, sizes 1–50 lines.useAgentMovement.ts(persona-only, just shipped 2026-05-23) — the new agent-driven walk runtime. Lives only in persona; world picks it up automatically post-merge.
Package.json semantics
| Aspect | World | Persona | Resolution |
|---|---|---|---|
| Scope | @openalice/* | @openalicelabs/persona-* | Rename all; update 48 imports in apps/web |
| avatar/core | No exports map | Full exports map | Adopt persona's; world's avatar becomes a re-export shim from core |
| engine | sideEffects:false + vitest | No flags | Add to persona on merge |
| multiplayer | sideEffects:false + agent-protocol dep | None | Add if WorldAgent stays |
| voice | depends on @openalice/engine | None | Persona's voiceChat needs optional engine adapter for ducking |
| world | depends on 4 world packages | depends on 3 persona packages | Update to persona scope; persona world adds useAgentMovement automatically |
Migration plan (4 phases — daylight work)
Phase A — pre-flight regression baseline (S, ~1–2 h)
- Branch
unify/character-stack-phase-a-baselinein both repos. - Run
pnpm --filter @openalice/world-web testandpnpm --filter @openalicelabs/persona-web test; capture pass/fail counts. - Smoke-test live world.blal.pro + persona.blal.pro manually: IslandScene loads, voice connects, avatar moves, weather cycles. Screenshot each, attach to PR body.
Phase B — file-by-file dedup (M, ~4–6 h + review)
For each of the 18 divergent files (listed above):
- Run
diff -ubetween world's and persona's copy. - Identify which version has the bugfix / perf win / extra feature.
- Port the win into persona's copy. Persona becomes canonical.
- Add a regression test where the win is non-obvious (e.g. soundManager dispose path).
- Decide on
WorldAgent(219 lines): promote to persona OR keep as a world-app local. Recommendation: promote — persona-world becomes "any world", world apps reuse. - Commit per logical merge:
feat(persona-engine): adopt world's soundManager dispose path, etc.
Branch: unify/character-stack-phase-b-dedup. Run engine + core tests, manual diff spot-check on 3–4 merged files.
Phase C — import sweep in openalice-world/apps/web (M, ~2–3 h)
48 @openalice/* imports across ~20 files in openalice-world/apps/web/src/{hooks,components,lib,app}/.
Batch rename via sed (run from openalice-world/apps/web/):
git grep -lE '@openalice/(avatar|engine|voice|multiplayer|world)' src \
| xargs sed -i \
-e 's|@openalice/avatar|@openalicelabs/persona-core|g' \
-e 's|@openalice/engine|@openalicelabs/persona-engine|g' \
-e 's|@openalice/voice|@openalicelabs/persona-voice|g' \
-e 's|@openalice/multiplayer|@openalicelabs/persona-multiplayer|g' \
-e 's|@openalice/world|@openalicelabs/persona-world|g'Verify clean:
git grep -E '@openalice/' src && echo "STRAGGLERS" || echo "clean"Update apps/web/package.json lines 13–18 — six workspace deps renamed from @openalice/* to @openalicelabs/persona-* with file: paths pointing into persona repo (or NPM once published). Update pnpm-workspace.yaml to include the persona path. Update docker-compose mount to include persona repo if not already.
Run pnpm install and pnpm --filter @openalice/world-web build; TypeScript catches anything sed missed.
Branch: unify/character-stack-phase-c-imports.
Phase D — package.json + cleanup (M, ~3–4 h)
- Decide on avatar package shape. Persona's
corecovers what world'savatarexports. Either: - (a) world-app imports@openalicelabs/persona-coredirectly,@openalice/avatarbecomes a thin re-export shim, OR - (b)@openalicelabs/persona-avataris added as an alias package that re-exports core. Recommendation: (a) — fewer packages, simpler. - Delete or deprecate
openalice-world/packages/{avatar,engine, multiplayer,voice,world}/. Two options: - (i) Delete outright (world has no other consumers). - (ii) Leave as stubs with// DEPRECATED — see @openalicelabs/persona-*comments + one-line re-exports for transition safety. Recommendation: (ii) for one release cycle, (i) after. - Consolidate tests. Move world's
__tests__directories into persona packages, OR leave as integration tests in apps/web. Recommendation: move package-level tests into persona; keep app-level integration in apps/web. - Atlas manifest — update
openalice-world/.atlas-deps.ymlruntime_deps to declare it consumes persona's packages. Updateopenalice-persona/.atlas-deps.ymlto note world as a consumer.
Branch: unify/character-stack-phase-d-final. Full E2E: apps/web build + dev server + world.blal.pro smoke test. PR for team review.
Risks + mitigations
| Risk | Likelihood | Mitigation |
|---|---|---|
World loses soundManager.dispose memory-safety win during merge | Medium | Phase B: explicit port + a regression test that asserts dispose nulls the singleton |
| Import sweep misses a file → build breaks | Low | Phase C: git grep -E '@openalice/' verification + tsc catches |
useAgentMovement (new tonight) coordinates badly with world's WASD player input | Medium | Phase B: wire isAgentDriving ref into useAvatarMovement to dampen keys; test side-by-side |
Voice ducking (@openalice/engine import) has no persona equivalent | Medium | Phase D: optional dynamic import in persona's voiceChat — engine becomes a runtime-detected dep |
| Tests fail because of FPS-specific assumptions | Medium | Phase D: classify tests as "world-app integration" vs "package unit"; move accordingly |
Indirect external consumers (other apps in monorepo) of @openalice/* | Low | Phase D: rg "@openalice/" /home/blal/projects/org-openalicelabs/ --type ts before deleting stubs |
What this unlocks
- Single source of truth for VRM + terrain + audio + voice + multiplayer — bug fixes land once.
- `useAgentMovement` ships to world for free — agent-walks works in
world.blal.prowithout re-implementing. - Live's persona iframe stays the same — already consumes persona's runtime; this just makes world consistent.
- Cleaner monorepo: world becomes thin (apps/web only); the character stack has one canonical home.
- Setup for Social to embed avatars later — same package surface, no further forks.
Effort summary
| Phase | Effort | Wall time | Sequencing |
|---|---|---|---|
| A — baseline | S | 1–2 h | First |
| B — file dedup | M | 4–6 h + review | After A |
| C — import sweep | M | 2–3 h | After B merged |
| D — package.json + cleanup | M | 3–4 h | After C merged |
| Total | — | 10–15 h over 1–2 days | linear |
Next actions
- Approval of this plan + decisions on the two open questions: - WorldAgent: promote to persona or keep world-local? - Avatar package shape: (a) re-export shim or (b) alias package?
- Phase A can start whenever — non-destructive baseline.
- Phase B is the long pole; need full daylight + concentration.
This file lives in atlas KB as the canonical work-plan. When execution starts, add commit refs inline so the trail stays linked.