Skip to content

Changelog

v5.0.403

Hotfix for a one-line typo in the v5.0.402 rename-detection backstop.

  • Fix: EmbodyExt.Update() rename-detect uses self.my, not self.ownerComp: The new rename-detection block added in v5.0.402 referenced self.ownerComp.ext.Envoy.RefreshRegistry(). EmbodyExt stores its owner COMP as self.my (line 82: self.my = ownerComp); only EnvoyExt uses self.ownerComp. I copied the pattern from EnvoyExt without verifying. Result: every Update() tick during a v5.0.402 save threw 'EmbodyExt' object has no attribute 'ownerComp', the warning got logged but the rename-detect path never actually fired -- the registry wouldn't walk forward on save. Caught immediately on first fresh-session check by inspecting dev/logs/Embody-5.402.toe_*.log. The Layer 2 walk-forward in the bridge masked the user-visible symptom (lookups still resolved to the new .toe), but the registry would have stayed perpetually keyed to the previous version. One-character fix; unit-test path was unaffected since the test code paths don't exercise this property reference.

v5.0.402

Three closely-related fixes for the registry that landed during follow-up testing of v5.0.401: dead-PID rows now garbage-collect on every write (catching the accumulation that hard-kills/force-quits/crashes leave behind), Update() watches for .toe basename changes as a backstop for execute.py's postSave hook in case it didn't reload, and the bridge's launch_td guard scans every alive instance by PID instead of relying on the (potentially stale) registry key.

  • Fix: registry GC -- dead-PID rows pruned on every write: Embody only deregisters from envoy.json on graceful shutdown (Stop()/onDestroyTD). Hard kills, force-quits, OS crashes, and Cmd+Q-without-Envoy-stop all leave entries behind that accumulate across sessions -- a long-running developer would routinely see 20-50 dead rows. Reported in this session: a registry with 28 entries, 27 dead. _writeEnvoyConfig now scans instances and removes any row whose td_pid is no longer alive (uses _isPidAlive -- os.kill(pid, 0), no syscall to verify it's actually a TD process, but PID recycling collisions self-correct on the next write since the new owner re-registers). Runs on every registry write -- Envoy startup, save-time RefreshRegistry, and the new Update() rename-detect path. The registry stays bounded automatically. Verified: a 28-row registry collapsed to 1 row on the first post-fix save.
  • Fix: EmbodyExt.Update() watches project.name and triggers RefreshRegistry: execute.py's onProjectPostSave already calls RefreshRegistry, but execute.py is a project-lifecycle script and its in-process reload behavior across edit-on-disk sessions is unreliable. The v5.0.401 fix could miss saves whenever the running TD held a stale copy of execute.py. New defensive backstop: Update() (which runs on every Refresh pulse and parameter change) caches _last_toe_name and compares to project.name each tick. On mismatch, it sets the new name and calls Envoy.RefreshRegistry(). EmbodyExt auto-reloads on source change, so this hook is reliable in a way that depending on execute.py reload isn't. Idempotent -- _writeEnvoyConfig short-circuits when the registry is already current.
  • Fix: bridge launch_td guard is PID-aware, not just key-aware: The v5.0.401 walk-forward in resolve_toe_path interacted badly with the v5.0.399 instance-specific guard. When the registry has a stale key (e.g. registered as Embody-5.400 even though the live .toe is now .401), the walk-forward correctly resolved the target to Embody-5.401, then the guard looked up instances["Embody-5.401"], missed (because the row is still keyed under .400), and let the launch proceed -- spawning a duplicate TD pointing at the same .toe. Triggered live during v5.0.401 verification. Fixed in handle_launch_td by adding a slow-path PID-aware scan after the fast-path key lookup: iterate every instance, skip dead PIDs, walk-forward each registered toe_path, and refuse if any resolves to the same target. Names the stale key in the error message so the user understands what to switch_instance to. Catches the stale-key edge case automatically.
  • Tests: 4 new in test_envoy_registry (TestRegistryDeadPidGC): test_dead_rows_pruned_on_write (28-row tempdir registry collapses to 1 after _writeEnvoyConfig), test_live_foreign_row_preserved (foreign live PID stays, dead rows go), constructed against synthetic envoy.json files in a tempdir + injected _isPidAlive predicate so the test runs deterministically without relying on actual machine state.
  • Tests: 2 new in test_envoy_bridge (TestBridgeLaunchTd*): test_launch_td_pid_aware_guard_catches_stale_key (registers Project-1.400 with our PID, walks forward to Project-1.401.toe, refuses with the .400 key in the message), test_launch_td_pid_aware_guard_ignores_dead_pids (registered toe walks forward to target but PID is dead -> fall through past the guard, fail on the executable check). Test file count stays at 50.

v5.0.401

envoy.json registry now walks forward across TD's save-time .toe version bump (Foo-5.398.toe -> Foo-5.399.toe), so the bridge keeps tracking the live instance instead of orphaning a stale entry. Two-layer fix: Embody re-registers under the new basename on save (proactive), and the bridge defensively iterates up to the highest-versioned sibling when an active entry's toe_path no longer exists. Plus a hotfix for the post-save call's incorrect self.port reference (caught in the same session: the v5.0.400 save itself surfaced the bug).

  • Feature: Embody-side rename walk-forward in the instance registry: TD's project.save() increments the trailing numeric segment of the .toe filename. The save handler then needs to update envoy.json so the bridge can keep talking to the same TD process under its new basename. Two-part change in EnvoyExt._instanceKey and _writeEnvoyConfig. The key computation now distinguishes "same PID, same toe_path" (idempotent re-register, returns the existing key) from "same PID, different toe_path" (rename in progress, returns the new basename so the caller can prune). The writer side runs an explicit prune pass after computing the key -- any other rows belonging to the current PID under different keys are deleted, so the registry walks forward instead of accumulating dead aliases. RefreshRegistry() is a new public method that re-registers from the live process state; called from onProjectPostSave in execute.py so the registry gets rewritten regardless of whether Envoy restarts (it does in Full mode but not Off/Export). Caught by an in-session repro: a save renamed Embody-5.398.toe -> Embody-5.399.toe but the registry stayed pointed at .398, leaving the bridge unable to reach the running TD on the next session restart. Now: registry follows the rename, stale row pruned in the same write.
  • Feature: bridge-side defensive walk-forward in resolve_toe_path: Layer 2 of the same fix, in case Layer 1 didn't fire (manual rename outside TD, save-as-version-up, Embody disabled at save time, etc.). New helper find_latest_versioned_toe strips the trailing <digits>.toe from a missing path to derive a prefix, scans the directory for siblings, and returns the path with the highest extant numeric suffix. resolve_toe_path now reads from instances[active] first (the multi-instance format -- previous flat-format-only behavior would silently return None for any modern config), falls back to legacy top-level toe_path, and walks the result through find_latest_versioned_toe so a stale registry entry still resolves to a usable file. The bridge does NOT rewrite envoy.json from this path -- registry mutation stays Embody's responsibility; the bridge just uses the corrected file in-memory and logs a warning.
  • Fix: RefreshRegistry() no longer crashes with 'EnvoyExt' object has no attribute 'port': Initial implementation read self.port, which is an attribute of the worker-thread EnvoyMCPServer, not the main-thread EnvoyExt. The actual runtime port isn't retained on the extension (it's a local in Start()), so RefreshRegistry() now reads it from envoy.json by looking up the row whose td_pid matches os.getpid(). Single source of truth (the registry itself) -- no instance attribute to keep in sync. The bug was harmless on Full-mode saves because Envoy restarts after the strip and re-registers correctly anyway, but it'd have been broken silently on Off/Export-mode saves where there's no restart. Caught immediately on the first save by the user.
  • Tests: 7 new in test_envoy_registry: covers _instanceKey directly. test_basename_used_when_registry_empty, test_existing_key_reused_when_toe_path_unchanged (idempotence), test_walks_forward_when_toe_path_changes_for_same_pid (the rename case), test_reclaims_own_basename_collision (PID's own row at the new name), test_appends_suffix_for_live_foreign_pid_collision (foreign live PID -> -2 suffix), test_reclaims_dead_basename (stale row reclaimed), test_old_pid_entry_not_reused_when_toe_changed (sanity).
  • Tests: 13 new in test_envoy_bridge: 7 in TestBridgeVersionIteration covering find_latest_versioned_toe (returns input on existence, walks forward, picks highest among many, no-siblings, unrelated files ignored, non-digit suffix no-walk, missing directory) and 5 in TestBridgeResolveToePath covering resolve_toe_path (multi-instance format, multi-instance walk-forward, legacy flat format, empty config, missing-active key). Plus test_launch_td_unrelated_td_running proving the v5.0.399 instance-specific guard correctly allows launching alongside an unrelated TD project.
  • Tests: 1 updated: test_launch_td_already_running rewritten for the v5.0.399 instance-specific guard. The old assertion looked for "already running" against any TD; new assertion sets up a registry entry where the target instance's PID matches os.getpid() and verifies the error names the specific instance, not a generic "TouchDesigner is already running". Test file count goes from 49 -> 50 (test_envoy_registry.py is new).

v5.0.399

New edit_dat_content MCP tool for token-efficient surgical edits to text DATs, plus a bridge multi-launch fix so Envoy can launch a TD instance alongside an unrelated TD project. Reported as token-cost feedback by Jeff.

  • Feature: edit_dat_content MCP tool — surgical text edits without round-trip cost: set_dat_content is full-replace by design — even a two-line edit in a 500-line DAT pays for the entire DAT's content in the tool call. Reported by Jeff in the Embody chat: typical agent edits were adding ~2k tokens for trivial changes inside large DATs. The new edit_dat_content(op_path, old_string, new_string, replace_all=False, confirm_wipe=False) tool mirrors Claude Code's Edit tool exactly: old_string must appear exactly once by default, otherwise the caller widens it with surrounding context for uniqueness or passes replace_all=True. Only the changed substring crosses the wire, so a 2-line edit in a 500-line DAT now sends ~2 lines instead of ~500. Text DATs only — table DATs go through set_dat_content(rows=...) since string matching across cells is a different beast. Refuses empty old_string (would match every position), refuses identical old_string/new_string (no-op), and reuses the v5.0.397 wipe guardrail: edits that would leave the DAT empty require confirm_wipe=True. Not-found errors include diagnostics (DAT length, row count, case-insensitive hint) so the agent can self-correct without a second get_dat_content round-trip. set_dat_content's docstring now points users to the new tool for partial edits.
  • Feature: bridge multi-launch — Envoy can launch alongside unrelated TouchDesigner projects: handle_launch_td previously refused to launch if any TD process was running, even an unrelated project on a different .toe. The instance registry has supported multi-instance since bridge v2, so the blanket guard was overly conservative. Replaced with an instance-specific check: only refuses if the target .toe's registered PID is alive (suggests switch_instance instead). Other TDs are now passed through cleanly. The macOS launch path also gained the -n flag (open -n -a TouchDesigner.app file.toe) — without it, LaunchServices reuses an existing TD window and spawns no new process, which silently broke multi-instance. The PID-detection step after spawn now diffs find_all_td_pids() against a pre-launch snapshot instead of returning pids[0], so the bridge correctly identifies the new TD's PID even with multiple TDs running. launch_td() (the helper) gained an optional existing_pids parameter for this; handle_launch_td snapshots before delegating.
  • Test debt: test_set_dat_content_clear updated for v5.0.397 wipe guard: The wipe guardrail shipped in v5.0.397 added a confirm_wipe=True requirement for clear=True calls with no replacement content, but the existing test_set_dat_content_clear regression test was missed in that release's test sweep — it's been failing since v5.0.397 because it called clear=True without the new flag. One-line fix to add confirm_wipe=True. Caught while running the new edit_dat_content suite.
  • Tests: 11 new tests in test_mcp_dat_content: test_edit_dat_content_basic (find-and-replace with unique match), test_edit_dat_content_requires_unique_match (refuses 3-occurrence match by default with explicit count in error), test_edit_dat_content_replace_all (opt-in replaces every occurrence with replacement count returned), test_edit_dat_content_not_found / test_edit_dat_content_case_insensitive_hint (diagnostic when only case differs), test_edit_dat_content_empty_old_string, test_edit_dat_content_identical_strings, test_edit_dat_content_rejects_table_dat (text-only enforcement), test_edit_dat_content_nonexistent, test_edit_dat_content_wipe_guard (refuses if result would be empty), and test_edit_dat_content_wipe_confirmed (accepts wipe with explicit flag, asserts content actually emptied). All 20 tests in the suite now green.
  • Docs: edit_dat_content listed in tool reference: docs/envoy/tools-reference.md and docs/envoy/index.md add the new tool entry. .claude/skills/mcp-tools-reference/SKILL.md (and matching template) get the full row with the partial-edit guidance and uniqueness/replace_all semantics. set_dat_content's row updated to recommend edit_dat_content for partial edits and reserves itself for tables, full rewrites, and intentional wipes. .claude/rules/skill-prerequisites.md, the td-api-reference SKILL description (and template), and text_claude.md's tool-loading checklist all add edit_dat_content alongside execute_python and set_dat_content since editing DAT contents may involve writing TD Python.

v5.0.398

Hotfix for a latent race condition that silently broke the first-install dialog flow on fresh-project drops. The bug was older than v5.0.397 — surfaced when a user finally tested a fresh-drop on a machine without a cached catalog.

  • Fix: Update() no longer races with EnsureCatalogs() on fresh-drop: When a user drops the release .tox into a brand-new project that has no .embody/catalog_<build>.json cached, CatalogManagerExt.EnsureCatalogs() kicks off a background scan and sets Embody.par.Status = 'Scanning defaults (X/N)' to show progress. That scan runs concurrent with the post-onCreate Verify → UpdateHandler → Update → _promptEnvoy chain. The chain sets _pending_envoy_prompt = True in Verify, then Update was supposed to consume it and schedule _promptEnvoy. But Update had if self.my.par.Status != 'Enabled': return — too strict. When the catalog scan won the frame race (which it usually did, because the scan starts at +45 and Update at +44 from onCreate, well within scheduler jitter), Status was already 'Scanning defaults (...)', Update returned early, the prompt flag was never consumed, and the Envoy opt-in dialog never appeared. User never got the chance to enable Envoy or initialize git — .embody/ ended up containing only the cached catalog JSON. Latent for many releases. Fixed by changing both gates (Update() and ReconcileMetadata()) from Status != 'Enabled' to Status == 'Disabled'. Embody is functionally enabled during scanning/testing — those transient Status values must NOT block normal operation. Reported by a fresh-drop test on Windows after the v5.0.397 release.
  • Tests: 2 new regression tests in test_smoke_release: test_update_consumes_pending_prompt_during_catalog_scan directly reproduces the race (sets Status='Scanning defaults', sets _pending_envoy_prompt=True, calls Update, asserts the flag was consumed). test_update_skips_only_when_disabled verifies the new contract — Update runs for every transient Status value (Scanning defaults, Scanning palette, Testing) and only short-circuits when Status is explicitly 'Disabled'. Both fail without the fix.

v5.0.397

Three independent improvements bundled together: a wipe guardrail on the set_dat_content MCP tool to prevent silent destruction of user content from malformed agent calls, a TDN at-risk filter that excludes TD-managed read-only DAT types from the save-time content-loss warning, and a deterministic settings serialization fix that closes issue #18. Plus a substantial test-debt cleanup that brings the previously-failing legacy tests back to green.

  • Feature: confirm_wipe guardrail on set_dat_content: The MCP tool is full-replace by design, but agents occasionally call it with empty text="", empty rows=[], or clear=True with no replacement content — silently destroying everything in the DAT. Reported by a user whose agent twice rebuilt the same DAT after wiping it without realizing. The handler now refuses any call whose result would be an empty DAT unless the caller passes confirm_wipe=True. The check inspects the resulting state, not just inputs, so legitimate atomic-replace calls (clear=True, text="hello") still work without the flag. Error message names the override and points back to get_dat_content as the proper read-modify-write workflow. A second guard refuses no-content calls (text=None, rows=None, clear=False) — same failure shape (silent confused success), refused the same way. Tests cover empty-text, empty-rows, the no-op case, atomic-replace pass-through, single-empty-row not-a-wipe, whitespace not-a-wipe, no-partial-mutation guarantee on rejection, and the explicit confirm_wipe=True override path.
  • Feature: TDN at-risk dialog skips TD-managed DAT types: The save-time "TDN Content at Risk" warning previously flagged every non-empty unexternalized DAT inside a TDN-strategy COMP, including TD-generated read-only DATs (Info DAT, WebRTC DAT, Folder DAT, Monitors DAT, device-discovery DATs, Error/Perform/Examine, etc.) whose content TD regenerates on cook. Users couldn't act on these warnings — the content isn't theirs to preserve. New _TD_MANAGED_DAT_TYPES denylist excludes the 19 known read-only generator types from the at-risk scan. Callback DATs (executeDAT, chopExecuteDAT, datExecuteDAT, panelExecuteDAT, parameterExecuteDAT, etc.) are intentionally NOT in the set — those hold user-authored Python and losing them silently is exactly what the warning exists to prevent. Reported by a user whose Moonshine projection-mapping project was getting noise from deform_info, keystone_info, and webrtc_dat operators on every save.
  • Fix: .embody/config.json is now byte-stable across saves (issue #18): _PERSISTED_PARAMS is a frozenset and Python's per-process hash randomization gave each TD session a different iteration order. _saveSettings used that order to populate the params dict and json.dumps preserved insertion order, so the file got a different (but valid) key ordering every session — producing a noisy diff on every git status even when no settings changed. Two surgical changes inside _saveSettings: iterate sorted(self._PERSISTED_PARAMS) and pass sort_keys=True to json.dumps. The frozenset is unchanged so O(1) membership checks elsewhere still work. First commit after the fix shows one-time noise as the on-disk file rewrites in sorted order; after that, stable. Reported by chrsmlls333.
  • Test debt: 28 stale .txt test files removed: Pre-existing duplicate test files alongside their .py counterparts in dev/embody/unit_tests/, leftovers from an earlier externalization format. Not referenced by any DAT or externalizations.tsv entry, but the test runner discovered both .py AND .txt from disk and ran every duplicated suite twice — bloating run times and obscuring real failures behind double-counted noise. Suite count for the full run drops from 103 discovered classes to 74 (the actual file count). No coverage lost; every .txt was byte-identical to its .py (or stale).
  • Test debt: test_ancestor_rename tearDown leak fixed (4 tests): All four "should succeed" assertions in _handleAncestorRename were failing intermittently because the test's tearDown cleaned dev/embody/unit_tests/_test_ancestor (a path nothing actually wrote to) instead of the real test-created prefix dirs (dev/embody/retval/, tblupd/, tdntest/, cancel_test/, conflict/, phaseA/, etc.). After a successful rename, the renamed-target dir was left on disk; the next run hit the (correct!) "Target directory already exists" guard in _handleAncestorRename and the test failed even though production code was working perfectly. New tearDown snapshots top-level dirs in dev/embody/ at setUp and removes any new ones at teardown, plus rmtrees the workspace dir under the sandbox for the no-ext-folder test path. All 19 tests in the suite now pass on consecutive runs.
  • Test debt: 3 envoy_bridge stubs converted to real tests + 1 deleted: TestBridgeV2DeferredStubs carried three raise SkipTest('depends on bridge v2 step N') placeholders left behind from when the bridge v2 features weren't shipped yet. All three features (local ping handler, find_all_td_pids, 3s initial probe + bridge-only fallback) have actually been live since v5.0.391 — the stubs were just stale TODOs that registered as ERRORs in the test runner. Replaced with two real tests for the local ping handler (request returns {result: {}}, notification produces no output) and four real tests for find_all_td_pids filtering (excludes own PID, excludes bridge processes, returns [] on TimeoutExpired / FileNotFoundError / pgrep no-match). Deleted the 3s-probe stub entirely — already covered by test_tools_list_bridge_only_when_td_down and test_full_mcp_handshake_when_td_down.
  • Test debt: 3 tdn_reconstruction palette tests aligned with current production contract: test_V03_palette_clone_flag_in_export, test_V07_clone_enablecloning_excluded_from_export, and test_V12_mixed_network_no_interference were written against the old palette-detection model where native widget COMPs (buttonCOMP, sliderCOMP) cloned from /sys/TDTox/defaultCOMPs/ were tagged with the palette_clone flag and had their children stripped from export. Production behavior was intentionally changed (commit e759b89) to exclude defaultCOMPs/* from palette-clone detection, so native widgets export as regular COMPs with full children — preserving any user customization inside the widget's internals. Tests rewritten to assert the new contract (no palette_clone flag, children exported, clone reference captured in per-op params, enablecloning correctly omitted as it matches its default). Section header docstring updated to describe the current model. The palette_clone flag remains reserved for true user palette clones from /sys/TDBasicWidgets and similar.
  • Tests: 23 new tests across 4 files: 11 in test_mcp_dat_content (wipe guardrail), 3 in test_tdn_safety_guards (TD-managed DAT filter), 3 in test_settings_persistence (issue #18 regression coverage — byte-stability + key sorting), 6 in test_envoy_bridge (ping handler + find_all_td_pids), minus 1 deleted stub. Test file count goes from 48 → 49.
  • Docs: set_dat_content and at-risk filter behavior: .claude/skills/mcp-tools-reference/SKILL.md, dev/embody/Embody/templates/text_skill_mcp_tools_reference.md (template counterpart), and docs/envoy/tools-reference.md updated to surface the new confirm_wipe? parameter with the wipe-guard contract. docs/embody/externalization.md Content Safety section now mentions the read-only DAT exclusion and the explicit callback-DAT inclusion, so users understand exactly which content types still trigger the warning.

v5.0.393

Hardens Envoy's Python-environment bootstrap so silent failures surface a useful textport message instead of an inscrutable No module named 'mcp.server' traceback at server-start time. Fixes the user-visible half of issue #17 (the macOS Library Validation half was retracted by the reporter after verifying TouchDesigner.app ships with com.apple.security.cs.disable-library-validation, so prebuilt PyPI wheels load fine in-process).

  • Fix: bootstrap failures now abort Start() with an explicit error: EmbodyExt._setupEnvironment previously returned None on every path including four silent-return failure paths (uv not findable, mcp version metadata unreadable, two try/except swallowed-error paths). EnvoyExt.Start called it fire-and-forget and proceeded to _runServer regardless, so any setup failure dropped the user into RuntimeError: MCP server failed on port 9870: No module named 'mcp.server' with no indication of why. _setupEnvironment now returns bool; each previously-silent return path logs an actionable message with platform-specific hints (e.g. "macOS GUI apps do not inherit shell PATH" when shutil.which('uv') comes up empty). Start() checks the return value, sets Envoystatus = 'Error: Python environment not ready', logs Aborting Envoy start -- See textport above for the underlying failure, and returns before _runServer runs. The other call site at _writeMCPConfig's venv-corruption recovery path is intentionally left ignoring the return — that path is already defensive with subsequent is_file() checks and exception fallback to system Python. Reported by Diego Chavez (issue #17).
  • Fix: final import mcp.server gate catches partial installs: New _verifyMcpImportable() helper runs importlib.import_module('mcp.server') after the install step succeeds — a populated site-packages is necessary but not sufficient (a partial install or load-time failure such as a missing native dep would still leave the server unable to start). The helper drops any cached failed mcp / mcp.* entries from sys.modules before retrying so a TD-process import attempt that previously failed gets a clean re-evaluation. On failure it logs Dependencies installed but mcp.server failed to import: <ImportError>. Inspect <site-packages> for partial installs and try deleting .venv/ to force a clean rebuild. and returns False — feeds straight into the Start() gate. Catches the exact symptom Diego would have seen if his bootstrap had ever gotten past the empty-site-packages failure
  • Verified locally: Ran the exact bootstrap subprocess sequence (uv venv .venv --python <TD-bundled-python> followed by uv pip install "mcp>=1.26.0" "attrs<25" --python .venv/bin/python) against TD's Python.framework/Versions/3.11/bin/python3.11 — both succeed cleanly on macOS Sequoia / Apple Silicon, 20 packages land in site-packages, prebuilt wheels load fine inside TD's process. Confirmed codesign -d --entitlements :- /Applications/TouchDesigner.app shows com.apple.security.cs.disable-library-validation is set on the host process (the framework's standalone python3.11 binary has no entitlements, which is why running .venv/bin/python from a terminal outside TD reports the dlopen Team-ID mismatch — irrelevant to actual Envoy startup since the wheels are loaded by TD's process, not by the standalone framework binary). Diego's "Problem 2" demand to default --no-binary pydantic-core,cryptography on macOS is therefore not just unnecessary but actively harmful — it would force every Mac user to install a Rust toolchain to compile from source for a problem that doesn't exist when wheels are loaded inside TD

v5.0.392

Single critical fix for a Windows-only venv-destruction loop that bricked Envoy on machines where TouchDesigner's GUI-process stdin handle isn't duplicatable.

  • Fix: subprocess.run from inside TD no longer raises [WinError 50] on Windows: Affected machines saw Embody's venv-bootstrap and verify-venv subprocess calls fail with OSError: [WinError 50] The request is not supported, traced to subprocess._make_inheritable calling _winapi.DuplicateHandle on the parent's STD_INPUT_HANDLE — TD's GUI process stdin handle is a console-buffer / non-duplicatable kernel object, so the duplicate fails before any child process is spawned. The verify-venv handler in EnvoyExt._writeMCPConfig treats OSError as "venv corrupt" and runs shutil.rmtree(.venv), so on every TD restart the auto-recovery destroyed a perfectly healthy venv, ran the bootstrap (which also failed with WinError 50), and left the user with no mcp package and a crashing MCP server. Fixed by passing stdin=subprocess.DEVNULL on every subprocess.run in the bootstrap path (3 sites in EmbodyExt._setupEnvironment / _findOrInstallUv) and the verify-venv path (2 sites in EnvoyExt._writeMCPConfig) — routes through NUL, which is duplicatable. Confirmed via textport repro: subprocess.run([sys.executable, '-c', 'print(1)'], capture_output=True) raised WinError 50 on the affected machine; the same call with stdin=subprocess.DEVNULL returned rc=0. Reported by Jason Latta.

v5.0.391

Three independent fixes shipped together: per-project TouchDesigner build pinning so the Envoy bridge can find the right install on a fresh clone, a thread-safety fix in the MCP update checker, and a 21-assertion cleanup of bridge tests that had been silently broken since the bridge v2 refactor.

  • Feature: .embody/project.json build pin: New committed metadata file (sibling of the existing gitignored .embody/envoy.json) records td_build — the TouchDesigner version the project was last saved with. EmbodyExt._writeProjectJson() writes it on onProjectPostSave and once at startup (onStart frame 80), idempotent so unchanged builds skip the write. Schema is intentionally minimal ({"td_build": "2025.32660"}) to leave room for additional project-level metadata later.
  • Feature: Bridge auto-discovers matching TD install: envoy_bridge.py now globs platform-specific install locations (C:\Program Files\Derivative\TouchDesigner.* on Windows, /Applications/TouchDesigner*.app on macOS via Info.plist CFBundleShortVersionString, /opt/derivative/touchdesigner-* on Linux) and picks the install matching project.json's td_build. Match policy: exact build → same year closest build (warns) → fall back to envoy.json's td_executable (warns) → newest installed (warns) → error with download link. Backward compatible — projects without td_build use td_executable from envoy.json exactly as before.
  • Gitignore: .embody/project.json is tracked: _configureGitignore switched the managed entry from .embody/ to .embody/* + !.embody/project.json. Existing projects auto-migrate on next Embody startup — the bare .embody/ line is added to STALE_ENTRIES and replaced with the negation pair. Project's own root .gitignore updated to match.
  • Fix: MCP update-check no longer trips TD thread conflict: _checkMCPUpdate() spawned a worker thread that called self.Log() directly on update detection — Log() reads absTime.frame, reads self.my.par.Verbose / Print, and appends to a FIFO DAT, all TD object access from a non-main thread, which TD's C++ runtime catches with a "THREAD CONFLICT" dialog naming the Embody COMP. On boot, _setupEnvironment runs twice (Start path plus venv-recovery), so two workers race to log "MCP update available" and the loser trips the dialog. Fixed by capturing owner_path outside the worker, pre-formatting the message string in the worker, and marshaling the Log call to the main thread via run("o = op(args[0])\nif o: o.Log(args[1], 'WARNING')", owner_path, msg, delayFrames=1). The if o: guard makes a rename/move between thread spawn and deferred fire a silent no-op.
  • Fix: bridge test debt — 21 stale assertions repaired: test_envoy_bridge.py was carrying assertions left behind by prior bridge refactors. TestBridgeForwardToHttp (17 tests) expected a pooled http.client.HTTPConnection (bridge._http_pool, bridge._http_pool_lock, _get_http_connection); the bridge had long since been simplified to a fresh urllib.request.urlopen per call. setUp / _make_conn helper replaced with _make_response; tests now mock urllib.request.urlopen directly and inspect the urllib.request.Request object. TestBridgeLog.test_log_includes_prefix was matching the literal '[envoy-bridge]' but the bridge format is '[envoy-bridge:<pid>]' — assertion now checks the stable '[envoy-bridge:' prefix. TestBridgeMainLoop (3 initial-connection-timeout tests) assumed v1 semantics where the bridge blocks on wait_for_envoy for arbitrary methods; v2 tries forward_to_http immediately and only errors when the forward call itself raises — tests now mock forward_to_http to raise OSError. Bridge tests went from 127/151 → 148/151 passing, zero failures, zero errors (3 explicit stubs skipped).
  • Tests: project.json + TD-install discovery coverage: New TestBridgeProjectJsonAndDiscovery class (15 tests) in test_envoy_bridge.py covers load_project_config() for missing / valid / malformed / non-dict cases, _parse_build() for valid / embedded / invalid inputs, and select_td_install() policy (exact match, same-year-closest, fallback-to-envoy-json, fallback-to-newest, no-pin behaviors, nothing-found with and without pin). find_td_installs() itself is platform-dependent; tested via the installs= injection point. Discovery sanity-checked live on the dev machine — picked up the installed 2025.32460 build, and _writeProjectJson correctly wrote the pin to .embody/project.json on onStart.
  • Dev rules: release-save procedure: .claude/rules/release-commits.md gained a new "Step 0: Save the Project" section documenting that project.save() must be called with no arguments. Passing a destination path causes TD's build-increment-on-save to parse the trailing build from your path instead of the current project.path, desyncing the .toe filename suffix from par.Version by one. Pre-setting par.Version manually has the same effect through a different route. Section also covers recovery if you've already mis-saved (rename .toe on disk; close TD without saving and reopen).

v5.0.386

Batch-confirm prompt for duplicate path detection — one dialog instead of N — so projects with several unresolved duplicate groups no longer spam the user with a modal per group on every save/refresh.

  • Feature: batch-confirm prompt for duplicate paths: When checkForDuplicates() finishes auto-resolving replicants, TD clones, and DATs inside cloned COMPs, any groups it still can't resolve now collect into a list. If 2+ groups remain, a single Duplicate Paths Detected dialog appears with three choices: Dismiss (skip for now, re-prompt next cycle), Review individually (falls back to the existing per-group prompt per group), or Auto-resolve all (N) (picks the first listed operator in each group as master; tags the rest with clone). Single-group case is unchanged — it goes straight to the original per-group prompt. Addresses user feedback that projects with many copy-pasted COMPs were hitting the per-group modal 5-10 times per save
  • Hardening: _messageBox list-of-responses for headless testing: The test harness's _smoke_test_responses storage dict now accepts a list of button indices per title (e.g. {'Duplicate Path Detected': [1, 1]}) in addition to the existing single-int form. List values are consumed front-to-back; the key is removed once empty. This unlocks multi-invocation test coverage of the new Review individually path where the per-group prompt fires multiple times within a single checkForDuplicates() call. Backward compatible — existing single-int seeds still work
  • Tests: New TestBatchResolution class in test_duplicate_handling.py with 6 tests covering the single-group shortcut, each batch-prompt button, per-group fallthrough, and the _autoResolveFirstAsMaster helper including empty-input safety. Full duplicate_handling suite: 56/56 passing. Verified live in the dev project with 3 fake groups — Auto-resolve all correctly kept alpha_1, beta_1, gamma_1 as masters and tagged the rest

v5.0.383

Clone detection fix for self-referencing masters (a common pattern for reusable UI components using iop.* expressions), and a cleaner list UI that moves the tree expand/collapse control into a dedicated column.

  • Fix: Self-referencing COMPs are masters, not clones: isClone() and isInsideClone() in EmbodyExt were misclassifying reusable-component masters whose par.clone evaluates to themselves (a standard pattern — a component COMP sets par.clone.expr = "iop.Components.op('MyComp')" so instances dropped elsewhere auto-sync). Before the fix, saving inside such a master would mark DATs as "inside a clone" and route them through the clone-side auto-resolve path, breaking externalization for the component's own authored contents. Both methods now treat par.clone is self (identity check on the evaluated op) as a master, not a clone. isClone() simplified from "does oper.name appear in the stringified clone value" string-match to the direct identity comparison. Added three unit tests in test_tag_management.py (test_isClone_self_reference_is_master, test_isInsideClone_self_reference_master_false, test_isInsideClone_self_reference_comp_itself_false) using expression-mode clone assignment to avoid TD's direct-assignment recursion
  • UI: Dedicated expando column in the externalization list: The tree-expand indicator used to be prefixed onto the network-path cell as a ▸ Name / ▾ Name string, which left the path column doing two jobs and misaligned when names varied in length. list_callbacks.py now renders a dedicated + / character in the leading 16-unit-wide expando column (previously hidden at width 0), leaving the network-path cell to show just the name centered-left with normal padding. Only rows with children get a character; leaf rows stay blank. Small visual change, noticeably cleaner at a glance
  • Chore: .gitignore entry for .release-drafts/: Local release-staging directory now ignored

v5.0.381

Global Perform Mode toggle suspends Embody/Envoy/TDN compute during live performance (Issue #13), auto-resolve for duplicate DATs inside active clones without prompting (Issue #15), ancestor-rename disk handling fixed so Move no longer fails with "source folder not found" (Issue #16), and new render-coordinate-system rules documenting TD's bottom-left origin convention (Issue #14).

  • Feature: Perform Mode (Issue #13, reported by Chris Mills): New Performmode toggle on the Embody COMP (and perform button in the toolbar) suspends all Embody/Envoy/TDN compute for the duration of a live performance. On enter, _enterPerformMode snapshots pre-state (Envoy running, keyboard listener active, exit tagger active) and stops Envoy directly, disables the keyboardin1 DAT and chopexec_exit_tagger, closes the manager window, greys out Envoy parameters, and sets Envoystatus = 'Perform Mode'. Guards added to Update, Refresh, Save, SaveTDN, SaveCurrentComp, TagGetter, ExternalizeProject, getDirtyCount, onProjectPreSave, onProjectPostSave, and Envoy's _onServerSuccess/_onServerError auto-restart. _exitPerformMode restores snapshot state and restarts Envoy if it was running. execute.py:onCreate clears Performmode = False on project open so the toggle never persists across sessions. Envoy parameter changes to Envoyenable/Envoyport/Aiclient are protected (never touched during Perform Mode, so config.json stays intact)
  • Fix: Auto-resolve duplicate DATs inside active clones (Issue #15, reported by Chris Mills): When a COMP with an externalized DAT inside is cloned, the master's DAT and the clone's DAT share the same relative path — producing a duplicate prompt on every save. _resolveDATsInClonedCOMPs() now auto-resolves these groups without prompting: DATs inside an active clone COMP are treated as references (clone-side), DATs in the master are kept as the master. Wired into the duplicate resolution flow in cleanupAllDuplicateRows() alongside the existing _resolveClonesByCloningAPI() handler
  • Fix: Ancestor rename no longer fails with "source folder not found" (Issue #16, reported by Chris Mills): _handleAncestorRename() was building disk paths from the raw operator-path prefix (e.g. /oldold) and passing that straight to project.folder / old. That works by coincidence when Externalizationsfolder is empty (the default — files write directly under the project root) because the op-path segment and the on-disk segment match. The moment Externalizationsfolder is pointed at a subfolder (say ext/), files actually live at project.folder / ext / old / ... but the rename code was still looking at project.folder / old / ... — so old_dir.exists() returned False and the user saw "Source folder not found." The method now composes Externalizationsfolder into the disk segment before every filesystem operation (Phase A rel_file matching, Phase C directory rename, Phase D table updates, TDN-strategy handling, user cancellation path all fixed). Returns bool so checkOpsForContinuity() can fall back to per-operator handling when the ancestor-level rename fails for any reason
  • Hardening: Clone detection null-safety: isInsideClone() and isClone() now use getattr(par, 'clone'/'enablecloning', None) with exception wrapping so DATs and operators that lack those parameters no longer raise during duplicate resolution. Also excludes DATs inside clone COMPs from the path-groups collected by _buildPathGroups() so replicant filtering and duplicate detection agree
  • UI: Perform button in toolbar: New perform textCOMP (Material Design icon) between Status and Disable buttons, wired to ToolbarExt._action_toggle_perform(). Tinted amber when active (face color driven by Performmode parameter, matching the Disable button's active-state pattern). Keyboard shortcut suppression, exit-tagger gating, and parexec routing to _enterPerformMode/_exitPerformMode all fire from the single Performmode toggle
  • UI: Full Envoy status string in toolbar: envoy_status widget now reads the full Envoystatus parameter ("Running on port 9870", "Off", "Error: ...", "Perform Mode") instead of just the port number. Width expanded 55 → 160 units. Text color unchanged (uses default Textcolor, not green). Window header min_width bumped 410 → 440 to accommodate the new button; title now concatenates Headerlabel + ' · ' + Envoystatus so project name and MCP status are visible from any docked pane
  • Rule: Render coordinate system (Issue #14, reported by Chris Mills): Added "Render Coordinate System" section to .claude/rules/td-python.md and expanded "TOP Pixel Access" in skills/td-api-reference/SKILL.md documenting TD's bottom-left origin convention. TOP.sample(x, y) y=0 is the bottom edge, GLSL gl_FragCoord.y=0 is the bottom, UV and crop/transform params are bottom-left, but TOP.numpyArray() returns rows top-to-bottom and PIL/OpenCV/panel coords are all top-left. Table + np.flipud() guidance added. Templates in dev/embody/Embody/templates/ synced so user projects get the new guidance on Embody initialization
  • Log: Fix pluralization in auto-resolve log line: _resolveDATsInClonedCOMPs() log message was using len(clones) != 1 as the plural guard, producing "0 DATs" with an errant s in the no-clones path. Changed to len(clones) > 1
  • Test: 48 test suites (+1): New test_ancestor_rename.py (680 lines) covers _detectAncestorRename threshold and prefix extraction, _handleAncestorRename Phase A/C/D on externalized COMPs, disk segment composition with ExternalizationsFolder, TDN-strategy handling, user cancellation, fallback to per-operator on failure, and full end-to-end rename flow with directory movement verification. test_duplicate_handling.py expanded (+70 lines) to cover _buildPathGroups replicant filtering, _resolveClonesByCloningAPI non-COMP handling, _resolveDATsInClonedCOMPs auto-tagging, and dialog-driven master/clone selection. test_tag_management.py expanded (+57 lines) to cover isInsideClone null-safety on DATs without par.clone and isClone active-vs-master discrimination

v5.0.376

Palette scan no longer triggers invasive palette popups (TDVR framerate warning, AutoUI widget-package dialog) on fresh-build startup, rebaked palette catalog for TD 2025.32460, and Issue #12 fix for false "locked content" warnings inside clones and replicants.

  • Fix: Palette scan skips invasive palettes (TDVR, AutoUI): CatalogManagerExt._startPaletteScan() now filters a small blocklist (tdvr, autoui) before loadTox. When palette_catalog bootstrap doesn't cover the current TD build, the runtime scan used to load every palette .tox into a hidden workspace — including TDVR (which unconditionally calls project.cookRate = 90 and pops a messageBox) and AutoUI (which pops a "Widget Package Required" dialog). Both were blocking main-thread modals that scared users into thinking Embody had taken over their project. Loss of palette-clone detection for these two components is acceptable — they're rare in TDN-diffed networks and were silently broken anyway. Single log line names what was skipped
  • Rebake: palette_catalog.tsv now covers build 099.2025.32460: Ran ExportPaletteCatalog() on current stable TD; the shipped bootstrap table now includes 261 palette components for 32460 alongside the existing 264 for 32280 (525 data rows + header, ~22 KB). Users on either build hit the bootstrap and skip the palette scan entirely on first load — no workspace creation, no loadTox calls, no popups
  • Fix: False "locked content" warnings inside clones and replicants (Issue #12, reported by Chris Mills): TDNExt._checkLockedUnexportedContent() now skips operators whose ancestor chain contains a clone master (clone + enablecloning both set) or a replicant template. Lock state inside clones is inherited from the master, not owned by the instance; lock state inside replicants is regenerated per-template by the replicator COMP. Warning the user about those paths is noise, not signal — and the paths (e.g. icon (TOP)) were especially confusing because they don't exist at the root level the warning referenced. Added helper _isInsideCloneOrReplicant(). Also switched summary from child.name to child.path so any remaining warnings point to an unambiguous location

v5.0.372

TDN master switch becomes a three-mode menu (Off / Export-on-Save / Roundtrip) replacing the short-lived Tdnenable toggle, new read_tdn MCP tool for 20-90× token-cost reduction on multi-operator reads, combined DAT+storage Content Safety dialog, palette-detection fix for native buttonCOMP operators, and a docs + landing page rewrite making the TDN value proposition explicit.

  • Feature: Tdnmode three-way menu: New Tdnmode menu on Embody's TDN page with three values. Off disables the entire TDN subsystem (no export, no reconstruction, no catalog scan — fastest startup for projects that don't use TDN). Export-on-Save (new default) writes .tdn files on save for diffs and AI context, but does not rebuild COMPs from .tdn on open — the .toe remains authoritative. Roundtrip (Experimental) is the full previous behavior: export on save plus reconstruct TDN-strategy COMPs from disk on open. Gates every TDN entry point (SaveTDN, Update() TDN loop, ReconstructTDNComps, pre-save strip, CatalogManager.EnsureCatalogs). Internal menuNames stay as off/export/full so persisted values and code references don't churn
  • Feature: Migration nudge for upgrading users: On first open after upgrade, projects saved with the legacy Tdnenable toggle see a one-shot dialog explaining the new mode and defaulting them to Export-on-Save (or offering a one-click restore of their previous Full behavior as Roundtrip). Stored flags (_tdn_mode_migration_shown + _tdn_migration_scheduled) prevent re-prompting and double-firing if _restoreSettings is called twice within the 60-frame defer window
  • Feature: read_tdn MCP tool: New MCP tool returns a COMP's live network as a TDN dict without writing to disk. Typically 20-90× fewer tokens than walking the same subtree via get_op + query_network thanks to default omission, type_defaults, and par_templates compaction. Intended as the preferred read path for LLM workflows exploring networks of more than ~3 operators. Works in all three Tdnmode values (reads live state, not disk). Scope cost via comp_path; cap with max_depth. Docstring enumerates when NOT to use (runtime values → get_parameter, cook errors → get_op_errors, DAT/TOP data → get_dat_content/capture_top, etc.). Conservative 5× floor verified in CI
  • Feature: Combined Content Safety check (Tdndatsafety → "Content Safety"): Pre-save safety gate now inspects both DAT content AND comp.storage for at-risk user data inside TDN-strategy COMPs, surfaced in one combined dialog. _findAtRiskStorage mirrors _findAtRiskDATs. Parameter renamed from "DAT Safety" to "Content Safety" to reflect the expanded scope. _STORAGE_SKIP_KEYS covers Embody's internal runtime keys (_tdn_stripped_paths, _tdn_palette_handling, migration flags, etc.) so only user-owned keys surface
  • Feature: Removed "Never Ask" dialog button: The Content Safety dialog no longer offers a single-click "Never Ask" footgun. Ignore remains available as a menu value on the Tdndatsafety parameter for power users who explicitly opt out, but the accidental-dismiss path that silently disarmed all future checks is gone. Dialog is now 3 buttons: Externalize DATs / Skip / Always Externalize. Skipped content is logged at SUCCESS level with the exact op paths and keys that were dropped
  • Fix: Palette detection false-positive on native buttonCOMP: TDNExt._isPaletteClone() was misclassifying stock TD operators like buttonCOMP as palette clones because every freshly-created COMP clones from /sys/TDTox/defaultCOMPs/<type> by default, and the /sys/ prefix matched the Strategy 2 heuristic. Detection now explicitly excludes /sys/TDTox/defaultCOMPs/ paths and 'defaultCOMPs' in the clone expression. Native COMP types export their internals normally; real palette clones (TDBasicWidgets, TDResources, actual Palette sources) still match
  • Fix: onProjectPostSave regression in Off/Export modes: Post-save used to early-return when _tdn_stripped_paths was empty — which never happened in the Tdnenable=True world but happens on every Off/Export save. The early return skipped _init_complete re-store, silently disabling every parexec callback for the rest of the session. Strip-restoration is now guarded by if stripped:; pane restore, _init_complete re-store, and the delayed Refresh.pulse() always run
  • Fix: Envoy restart conditional on strip having happened: Post-save Envoy restart was made unconditional during the above fix, which meant every Off/Export save was needlessly tearing down and restarting the MCP server thread. Now gated on stripped and Envoyenable.eval() so restart only fires in Roundtrip mode where the extension actually reinitialized
  • Fix: Cancel path on Off transition no longer double-logs: When a user flips Tdnmode to Off with tracked TDN COMPs and picks Cancel, the revert to export now happens with parexec suppressed so the transition handler doesn't re-fire and emit a misleading "mode: Export-on-Save" INFO immediately after the "cancelled by user" message. Also silences the "TDN disabled" log when flipping to Off with zero tracked COMPs
  • Perf: Catalog load gated on Tdnmode != 'off': CatalogManager.EnsureCatalogs() skipped entirely when Tdnmode = Off. The catalog is consumed exclusively by TDN export compaction and palette-clone detection — both dormant in Off. Saves the op-type scan + divergent-defaults probe at startup for users who don't need TDN
  • Docs: TDN Strategy section rewrite: docs/embody/externalization.md replaces the binary Tdnenable narrative with a three-mode table + a new Why TDN subsection covering file size/density, git three-way merge, PR review, cross-version portability, CI/CD schema validation, and the 20-90× MCP token cost reduction. Grounded in real TDNExt code paths (default omission, type_defaults, par_templates) and actual .tdn file sizes. configuration.md, getting-started.md, troubleshooting.md updated to match. In-app help text on Tdnmode and Tdndatsafety rewritten. read_tdn added to every MCP tool catalog page. /sys/TDTox/defaultCOMPs/ exclusion documented in the TDN specification. Migration nudge described in the config reference
  • Docs: Landing page (embody.tools) positioning rewrite: web/embody/index.html TDN Strategy feature card reframes TDN as a mirror of the .toe rather than a replacement. The "bidirectional sync" pillar becomes two pillars — export on save (the default) and roundtrip (experimental). web/tdn/index.html Embody pillar softened to match. web/envoy/index.html adds a new read_tdn feature card highlighting the 20-90× token reduction; tool count updated from 46 to 47. Sample TDN JSON generator strings bumped to Embody/5.0.372
  • Test: 47 test suites (+3, 1184 test cases): test_tdn_mode (15 tests covering all three modes, gating, reconstruction/SaveTDN guards, regression guards for _init_complete and Envoy restart), test_tdn_safety_guards (7 tests covering _findAtRiskStorage, combined dialog, Never-Ask removal, skip logging), test_mcp_tdn_tools (5 tests covering read_tdn round-trip, mode agnosticism, DAT content toggle, and a token-budget regression with a conservative 5× CI floor)

v5.0.362

Palette handling control during TDN export, CatalogManager robustness on fresh project drops, palette catalog portability and log-level fixes.

  • Feature: TDN palette handling (Tdnpalettehandling): New menu parameter on Embody's TDN page controls how palette COMPs are handled during TDN export. Ask (default) prompts on first encounter per COMP with a four-button dialog — Black Box (this COMP), Full Export (this COMP), Black Box for All (flips the project-wide par), Full Export for All (flips the project-wide par). Black Box always references the palette and skips internal children (correct for stock palette COMPs; lets upstream Derivative palette updates flow through on round-trip). Full Export always exports all internals (for heavily customized palette COMPs). Per-COMP decisions are persisted via comp.store('_tdn_palette_handling', …), so you aren't re-prompted for the same COMP. Implementation: TDNExt._resolvePaletteHandling(), TDNExt._promptPaletteHandling() consult per-COMP storage → par value → prompt
  • Fix: CatalogManager on fresh project drops: EnsureCatalogs() is now called from execute.py:onCreate at frame 45 in addition to onStart. Previously new users dropping the .tox had empty divergent defaults and broken palette detection in their first session — the catalog only loaded on project reopen
  • Fix: Catalog scan stall ("N/N" forever): CatalogManagerExt._log() was missing a level parameter. A v5.0.358 logging call passing 'DEBUG' as second arg caused a TypeError that silently killed _finalizeScan, leaving catalog scans stuck with no catalog written. _log(msg, level='INFO') now accepts optional level
  • Fix: Scan finalize defensively in-band: _processChunk and _processPaletteChunk now finalize the scan when the queue empties instead of relying on a scheduled run(delayFrames=1) callback for the final tick. Defends against lost callbacks during heavy concurrent startup (venv creation, dialog auto-response, Envoy server start)
  • Fix: palette_catalog tableDAT portability: Both the DAT's file par and the row in externalizations.tsv had an absolute path (broken on other machines). Now uses relative embody/Embody/palette_catalog.tsv + syncfile=True + file.readOnly=True, matching the divergent_defaults pattern
  • Rename: CheckAndScan()EnsureCatalogs(): Clearer intent-verb name. Method is now idempotent — safe to call repeatedly, returns early when already populated
  • Fix: Catalog scan errors demoted to DEBUG: Abstract base types (td.CHOP, td.DAT, etc.) that can't be instantiated bare were logging at INFO on every startup. Non-actionable for users
  • Fix: Gitignore migration noise: .envoy-tools-cache.json removed from stale-entries migration list — was being flagged on every startup despite being intentionally kept
  • Rule: Naming — Methods, Functions, Operators: New section in td-python.md + template covering intent-verb naming, avoiding CheckAndX/DoStuff/implementation-leakage patterns, boolean is/has/can phrasing, public-vs-private conventions
  • Docs: Updated configuration.md, externalization.md, TDN specification.md, in-app help text, and externalize-operator skill to cover palette handling and the shipped palette catalog mechanism
  • Test: 44 test suites (+1, 10 new G01-G10 tests in test_tdn_palette_catalog covering the palette handling resolver, prompt flow, per-COMP storage override, and end-to-end export behavior)

v5.0.356

Palette catalog detection, animationCOMP keyframe preservation, external wire preservation across TDN strip/rebuild, Envoyenable startup fix.

  • Feature: Palette component catalog: CatalogManagerExt now walks TD's shipped palette directory after the op-type scan, loads each .tox into a temp workspace, and records {name: {type, min_children}}. TDNExt._isPaletteClone() uses this catalog as the primary detection method (name + OPType + child-count floor), falling back to the clone-expression heuristic (now covers TDBasicWidgets in addition to TDResources/TDTox//sys/). Catches palette components whose clone reference was never set while avoiding false positives from user COMPs that happen to share a palette name
  • Feature: animationCOMP keyframe preservation: DATs inside an animationCOMP (keys, channels, graph, attributes) always export their content regardless of the include_dat_content option. Previously these read-only-looking tableDATs lost all keyframe data on TDN round-trip
  • Fix: External connection preservation across TDN strip/rebuild: Wires from external siblings into a TDN-strategy COMP's own input/output connectors (backed by internal in*/out* operators) were severed when the COMP's children were destroyed during save's strip/restore cycle, cold open, or manual reimport. StripCompChildren now captures external wires via comp.store() before destruction; ImportNetwork(clear_first=True) restores them after rebuild (and also captures live wires directly when called without a prior strip)
  • Fix: Envoyenable disabled on every startup: init() stored _init_complete immediately after setting Envoyenable = False, but TD defers onValueChange callbacks to the next cook — so parexec processed init's own Envoyenable=False change and called Stop(). _init_complete is now stored by _restoreSettings after restoration completes (or immediately on its early-return paths), keeping parexec suppressed through the deferred callbacks
  • Fix: Catalog path mismatch: CatalogManagerExt._findProjectRoot() now delegates to EmbodyExt._findProjectRoot() which walks up from project.folder looking for .git. Previously project.folder (often dev/) differed from the git root and produced duplicate catalogs under different paths
  • Fix: Abstract type scan rejection: td.CHOP, td.COMP, td.DAT, etc. are abstract base types with suffixes matching _FAMILIES but aren't creatable. Added _ABSTRACT_TYPES filter to skip them during catalog scan
  • Test: 43 test suites (+2): test_tdn_palette_catalog (catalog lookup, child-count floor, TDBasicWidgets heuristic, animationCOMP round-trip), test_tdn_external_connections (strip+import restore, live-wire capture, deleted-sibling tolerance)
  • Gitignore: Added .envoy-tools-cache.json (bridge tool cache — runtime artifact)

v5.0.354

Consolidate all Embody/Envoy runtime files into a single .embody/ folder.

  • Refactor: .embody/ folder consolidation: All auto-generated runtime files now live in one gitignored folder instead of scattered dotfiles at the project root. .envoy.json.embody/envoy.json, .embody.json.embody/config.json, .envoy-tools-cache.json.embody/envoy-tools-cache.json, .claude/envoy-bridge.py.embody/envoy-bridge.py. Makes Envoy fully client-agnostic -- no Envoy artifacts in .claude/
  • Migration: automatic upgrade from old paths: On first Envoy start after upgrade, existing config files are read from old locations, seeded into .embody/, and old files removed. .gitignore stale entries (.envoy.json, .embody.json, .claude/envoy-bridge.py, etc.) are automatically replaced with a single .embody/ entry
  • Fix: bridge path resolution: resolve_toe_path(), _heartbeat_path(), _init_log_file(), _find_stale_bridges(), and _validate_and_resolve() now correctly resolve paths relative to the git root (one level up from .embody/) instead of the config file's parent directory
  • Docs: Updated architecture, setup, claude-code, tools-reference, configuration, getting-started, multi-instance skill, and td-connectivity rule to reflect new paths

v5.0.352

Fix Envoy failing to start after Embody upgrade (delete old COMP, drop new .tox).

  • Fix: Envoy restart counter not resetting on upgrade: When the old server's port wasn't released in time, auto-restart exhaustion left _restart_count stuck above MAX. The next manual Envoyenable toggle immediately hit the limit and forced itself back to False, making the toggle appear to "do nothing." Stop() now always resets _restart_count, even when envoy_running is already False
  • Fix: Upgrade-path port race: Verify() deferred Start() by only 10 frames (~0.17s) after the old COMP was deleted -- too short for uvicorn to fully release its listener socket. Increased to 60 frames (~1s)
  • Fix: Port reclaim timeout too short: _findAvailablePort() waited only 0.5s for a force-closed port to become available. Increased to 1.5s to accommodate uvicorn's shutdown sequence

v5.0.351

Creation-defaults catalog, stdin-based bridge lifecycle, Envoy resilience hardening.

  • Feature: Creation-defaults catalog (CatalogManagerExt): TD's p.default lies for dozens of parameters (e.g., cameraCOMP tz: p.default=0 but creation value is 5). Embody now scans all creatable op types at startup (1-2 ops/frame, non-blocking), writes a per-build catalog to .embody/, and uses actual creation values for TDN export/import. Fixes silent data loss where user-set values matching the wrong default were omitted from export
  • Feature: Cross-build default patching: When opening a project exported on a different TD build, the CatalogManager compares catalogs and patches any parameters whose creation defaults shifted between builds. Shows a summary dialog of all corrected values
  • Feature: Divergent defaults fallback: Embedded divergent_defaults.tsv table provides bootstrap data for known TD builds. On-the-fly probing handles unknown builds by creating temp operators and comparing p.val vs p.default
  • Fix: Bridge orphan detection: Replaced ppid-based orphan watchdog (broken under VS Code extension host, which outlives sessions) with stdin pipe POLLHUP detection via select.poll() (macOS/Linux) and PeekNamedPipe (Windows). Bridges now exit reliably when their Claude Code session closes
  • Fix: Bridge stale process cleanup: Heartbeat files (envoy-bridge-{pid}.heartbeat) replace parent-PID heuristics for detecting stale bridges. Phase 1 kills bridges with stale heartbeats (>60s old); Phase 2 falls back to legacy orphan check for pre-heartbeat bridges
  • Fix: Bridge heartbeat simplification: Replaced dynamic fast/slow heartbeat cadence (5s/30s) with fixed 10s interval. Removed HTTP connection pool (reverted to simple urllib.request.urlopen per call) — the pool caused persistent "not responding" errors from half-closed http.client connections
  • Fix: Envoy queue persistence across save cycles: Request/response queues now survive extension reinit during Ctrl+S by persisting in sys._envoy_queues. Prevents lost MCP requests during the strip/restore window
  • Fix: Envoy auto venv recreation: Corrupted venv (broken Python path after TD upgrade) is now auto-recreated once per session instead of just logging a warning
  • Fix: ClientDisconnect suppression: Added starlette ClientDisconnect to suppressed exceptions alongside BrokenResourceError/ClosedResourceError. Prevents traceback floods from destabilizing uvicorn's event loop during extension reinit or tab close
  • Fix: Scan workspace cleanup: On-the-fly default probe workspace (_defaults_workspace) is now destroyed after each export. Previously leaked empty baseCOMPs that accumulated in the Embody COMP across saves
  • Improved: Em-dash to double-dash: Systematic to -- replacement across all Python source files for cross-platform DAT encoding safety
  • Improved: FastMCP log filtering: Suppresses empty "Received exception from stream:" messages from recycled bridge connections
  • Test: 41 test suites with 4 new divergent-defaults tests (cameraCOMP tz, lightCOMP tz, renderTOP resolution round-trip, false-positive prevention)

v5.0.336

Batch MCP operations, Envoy auto-restart on crash and save, 46 MCP tools.

  • Feature: batch_operations MCP tool: Combine multiple tool calls into a single request — positions, connections, parameters, flags, etc. Stops on first error, returns per-operation results. Cuts token overhead and latency for repetitive operations
  • Fix: Envoy dies on Ctrl+S: The save cycle's TDN strip/restore killed the server thread via extension reinit, leaving status stuck on "Running" with a dead port. onProjectPostSave now explicitly restarts Envoy after restoration completes
  • Fix: Envoy auto-restart on crash: Server thread failures (SuccessHook/ExceptHook) now trigger automatic restart with exponential backoff (1s, 2s, 3s) up to 3 attempts. Counter resets after 2 minutes of stable uptime. Manual Stop() resets the counter
  • Rule: Batch repetitive MCP operations (CLAUDE.md #12): Never make 3+ individual calls to the same tool — use batch_operations or execute_python instead
  • Test: 41 test suites including new test_mcp_batch (9 tests covering success, error handling, nested prevention, practical create+query patterns)

v5.0.330

Envoy bridge v2: proactive reconciliation, multi-session safety, and zero forced restarts. The bridge now survives TD crashes, instance switches, and multi-session concurrency without requiring Claude Code session restarts.

  • Feature: Background reconciler thread: Polls .envoy.json every 1 second (unconditionally, regardless of connection state) and pings the backend every 5–30 seconds (dynamic backoff). Detects instance switches within seconds — opening a new TD instance mid-session automatically routes MCP calls to the new instance
  • Feature: Disk-based tool cache: Persists the full tool list to .envoy-tools-cache.json so new sessions always start with all 45+ tools, even if TD hasn't finished loading. Works around Claude Code's list_changed notification bug (#13646)
  • Feature: HTTP connection pooling: Replaced per-request urllib.request.urlopen() with a persistent http.client.HTTPConnection per URL. Eliminates socket churn that was causing ClientDisconnect tracebacks in starlette and crashing Envoy's HTTP server under load
  • Feature: Dynamic heartbeat backoff: Pings every 5s while unstable or recently changed, slows to 30s once connected stably for 30+ seconds. Reduces textport noise by 6x in steady state
  • Feature: Proactive TD process discovery: find_all_td_pids() scans for new TouchDesigner processes every heartbeat, forces config re-read when new TDs appear. Filters out bridge processes that use TD's bundled Python (false positive fix)
  • Feature: notifications/tools/list_changed emission: Bridge advertises listChanged: true and sends the MCP notification on every backend state transition and explicit instance switch
  • Feature: Local ping handler: Answers MCP ping requests locally with zero latency, regardless of backend state
  • Feature: Multi-session safety: kill_stale_bridges() now checks parent PID before killing peers — only orphans (parent dead/reparented to launchd) are terminated. Multiple Claude Code sessions can safely coexist against the same project
  • Fix: Port conflict detection in multi-instance startup: _findAvailablePort() now checks the .envoy.json registry in addition to socket probes, preventing two TD instances from racing on the same port during near-simultaneous startup
  • Fix: Restart loop on port fallback: Removed Envoyport parameter update during Start() that triggered parexec.py Stop+Start cycle when the port shifted (e.g., 9870→9871)
  • Fix: Ghost TD detection: find_all_td_pids() now excludes bridge processes whose cmdline contains envoy-bridge, preventing false "TD is alive" reports when only bridge processes remain
  • Fix: Orphan watchdog hardening: Added is_process_alive(parent_pid) belt-and-suspenders check alongside ppid comparison, catches cases where ppid doesn't update immediately on reparenting
  • Improved: 3-second initial probe (was 60s): First tools/list response returns in ≤3 seconds with the best available tools (live, cached, or bridge-only). Reconciler handles recovery in the background
  • Improved: Single-attempt forwarding (was 4 retries): Failed MCP forwards return immediately instead of blocking 7.5 seconds on retries. The reconciler drives reconnection
  • Improved: PID-tagged log lines: [envoy-bridge:PID] format makes multi-session logs distinguishable
  • Improved: PID-tagged temp files: atomic_write_json() uses per-PID temp files to prevent collisions between concurrent bridge processes
  • Improved: Server-side log filter: Suppresses FastMCP's per-request Processing request of type PingRequest messages from flooding TD's textport
  • Test: 136 bridge unit tests across 19 suites, covering BridgeState locking, tool hash detection, reconciler state transitions, listChanged capability, cache hits, stdout serialization, single-attempt forwarding, and connection lifecycle

v5.0.320

TDN v1.3: parameter sequence round-trip + companion DAT handling. Operators with resizable parameter blocks (mathmixPOP, glslPOP, constantCHOP, etc.) and companion DATs (GLSL _pixel/_compute/_info, Timer/Script CHOP _callbacks, Ramp TOP _keys, etc.) now round-trip cleanly through TDN export/import.

  • Feature: TDN parameter sequence support (TDN v1.3): Operators with built-in parameter sequences (mathmixPOP Combine blocks, glslPOP/glslTOP uniform sequences, attributePOP attribute blocks, constantCHOP channel blocks, etc.) now export their sequence data in a new sequences key and restore it on import. Previously, adding parameter blocks (e.g., a new Combine block on mathmixPOP) would silently lose the added blocks after TDN round-trip
  • Feature: Custom parameter sequence support: Custom sequences defined via page.appendSequence() are now round-tripped correctly. Template parameters are exported with their base name and a sequence field; on import, blockSize is set from the template par count before numBlocks populates the block instances. Includes a fallback resolver for custom-sequence block parameters where TD's block.par.{base} lookup returns None
  • Feature: Read-only DAT detection: Auto-generated companion DATs (e.g. glsl1_info, popto1) that reject dat.text = ... writes are now probed at export time and tagged with dat_read_only: true. Their content is excluded from the export, and importers no longer log "not editable" warnings when restoring them. Older .tdn files without the flag are also handled silently
  • Fix: Parameter cache silently dropping sequence parameters: _buildParCache() cached exportable parameters per OPType from the first instance encountered. Sequence parameters with dynamic names (e.g., comb2oper on a 3-block mathmixPOP) were silently skipped on other instances whose block count exceeded the cached set. Sequence parameters are now excluded from the flat parameter cache and handled by the dedicated sequence export path
  • Improved: Import Phase 2.5: New _expandSequences() phase runs between custom parameter creation (Phase 2) and parameter value setting (Phase 3), ensuring dynamically-created sequence parameter slots exist before values are applied
  • Improved: Network layout rule — Docked Callback DATs: New section in .claude/rules/network-layout.md (and the matching template) defines a deterministic placement formula for the companion DATs that TD auto-spawns and docks to operators (chopExecuteDAT, glsl info DATs, keyboardinDAT, etc.). Includes a center-out alternation pattern and a procedure for repositioning every dock after create_op
  • Test: Sequence round-trip tests: 12 new tests in test_tdn_sequences.py covering export format, round-trip fidelity, expression values, nested COMPs, type_defaults exclusion, and backward compatibility
  • Test: Companion DAT round-trip tests: 14 new tests (Section W) in test_tdn_reconstruction.py covering GLSL TOP/multi/POP/copy/advanced companions, Timer/Script CHOP/SOP/DAT callbacks, Ramp TOP keys, read-only info DAT handling, and a comprehensive no-duplicates check across all companion-creating ops

v5.0.310

Fix first-time Envoy setup permanently stuck on "Enabled + Disabled" (issues #8, #9).

  • Fix: Envoy permanently stuck "Disabled" after first-time install (GitHub issue #9): _init_complete was stored as an instance attribute on EmbodyExt, destroyed when file sync recompiled DATs during first-time setup. Parexec silently dropped all parameter changes — including Envoyenable = True — so Start() was never scheduled. Moved _init_complete to COMP storage (.store()/.fetch()) which survives extension reinit. Added pre-save unstore and post-save re-store to prevent baking into the .tox
  • Fix: Start() status guard self-poisoning (GitHub issue #9): EnvoyExt.__init__ set Envoystatus = 'Starting...' before deferring Start() by 30 frames. Start() then saw 'Starting...' in its status guard and assumed another start was in progress — permanent deadlock. Removed the premature status from __init__; narrowed the guard to only block on 'Running' (actual server activity), not 'Starting...' (UI hint)
  • Fix: .gitignore and .gitattributes not generated on first-time git init (GitHub issue #8): Git config files are now created inside _checkOrInitGitRepo() immediately after git init succeeds, instead of relying on Start() which may not run in the same session
  • Fix: Type error in Start() git config (pre-existing): _configureGitignore/_configureGitattributes expect a Path object but Start() passed a string from COMP storage. Wrapped with Path() conversion
  • Improved: Start() status guard visibility: Upgraded the Envoystatus backup guard from DEBUG to WARNING so state inconsistencies are visible in logs

v5.0.305

Replicant duplicate detection fix (issue #4 update), TDN export improvements, ExternalizeProject dialog enhancement.

  • Fix: Replicant duplicate detection (GitHub issue #4 follow-up): _buildPathGroups() now filters out replicants alongside clones, preventing replicator outputs from entering the duplicate detection flow. Previously, 100 replicants sharing the same externaltox path would trigger a massive popup with 100+ buttons. Added _resolveReplicants() safety net that auto-tags replicants as clones without prompting if they reach checkForDuplicates() through another code path
  • Improved: ExternalizeProject dialog: Expanded the "Externalize Full Project" dialog with clearer descriptions and new combined options (TOX + Project TDN, TDN + Project TDN) that externalize operators and also export a project-wide .tdn snapshot in one step
  • Improved: TDN export source_file field: All TDN exports now include the originating .toe filename for traceability
  • Improved: Stable project TDN filenames: New _stripBuildSuffix() strips the auto-incrementing build number (e.g. .302) from project names, so root TDN exports produce a stable filename across saves (e.g. Embody-5.tdn instead of Embody-5.305.tdn)
  • Test: Replicant handling tests: 4 new tests in test_duplicate_handling.py covering _resolveReplicants, isReplicant, and replicator integration with _buildPathGroups
  • Test: TDN file I/O tests: 7 new tests in test_tdn_file_io.py for _stripBuildSuffix edge cases and source_file export verification

v5.0.302

Fix duplicate path clone detection (issue #4), config file location (issue #5), Envoy startup flow on fresh .tox install.

  • Fix: Clone assignment for duplicate paths (GitHub issue #4): Rewrote duplicate detection to use group-based path mapping (_buildPathGroups) and TD's .clones/par.clone API for automatic master identification. COMPs that are clones of each other are resolved silently; non-clone duplicates show a single per-group dialog with Dismiss option. Eliminated infinite cancel loop and wrong-operator tagging
  • Fix: Config files written to home directory (GitHub issue #5): Bounded _findProjectRoot() and _checkOrInitGitRepo() walk-up to stop at Path.home(), preventing accidental discovery of unrelated git repos (e.g. dotfiles in ~). Added _git_prompt_active guard against concurrent git dialogs
  • Fix: Envoy auto-start on fresh .tox drop: EnvoyExt.__init__ was scheduling Start() based on the baked Envoyenable=True before init() could reset it. Added _init_complete guard so auto-start only fires during extension reinit in a running session, never on fresh install. Removed _setupEnvironment() from EmbodyExt.__init__ (now runs inside Start())
  • Fix: Envoy opt-in prompt not appearing: _restoreSettings() finding a leftover .embody.json caused Verify() to skip the "Enable Envoy?" dialog. Fresh installs (empty externalizations table) now always prompt, regardless of prior settings files
  • Fix: Sequential dialog flow: Moved git repo check into _enableEnvoy() so it runs immediately after the user clicks "Enable Envoy" — before deps install. Start() now uses silent _findGitRoot() and never shows dialogs
  • Fix: Runtime-only storage baking into .tox: onProjectPreSave now unstores _git_root, _tdn_stripped_paths, and _tdn_pane_restore — these are session-only values that caused spurious warnings (e.g. "Post-save restore: .tdn file missing: unit_tests.tdn") when baked into the release .tox
  • Fix: parexec SyntaxError on save: Fixed non-ASCII bytes (smart quotes, em dashes) in parexec.py that caused SyntaxError when TD reads externalized files with CP1252 encoding
  • Improved: _restoreSettings() kick_envoy parameter: onStart() passes kick_envoy=True to defer Envoy start after settings restore; Verify() (onCreate path) uses default kick_envoy=False since it owns the Envoy startup flow
  • Test: Duplicate handling tests: 5 new tests in test_duplicate_handling.py covering _buildPathGroups, _resolveClonesByCloningAPI, group dialog, and user-selects-master flow
  • Test: Smoke release fix: test_envoy_server_running_if_enabled now checks Envoystatus parameter (survives extension reinit) instead of envoy_running store
  • Docs: Updated duplicate path handling section in externalization.md (39 test suites, 1390 tests)

v5.0.278

Fix folder change crash, regression tests.

  • Fix: Changing externalization folder deletes target directory (GitHub issue #3): When changing the Folder parameter, Disable() would fall back to project.folder when the previous folder was empty, then deleteEmptyDirectories would walk the entire project tree and delete the newly-created target directory. UpdateHandler then failed with FileNotFoundError. Fixed by guarding all directory cleanup to never operate on project.folder, and switching os.mkdir to os.makedirs(exist_ok=True) for robustness
  • Regression tests: Two new tests in test_custom_parameters.pytest_zz_folder_10_empty_dir_survives_disable reproduces the exact issue #3 scenario, test_zz_folder_11_disable_empty_prev_skips_project_folder verifies empty prevFolder doesn't walk project.folder (39 test suites)

v5.0.277

Manager UI improvements, new keyboard shortcut, consistent terminology.

  • "Update current COMP" toolbar button: New button (floppy disk icon) in the toolbar directly after "Update externalizations", calls SaveCurrentComp() — equivalent to Ctrl+Alt+U. Visible in both full and minimized manager views
  • Ctrl+Shift+R keyboard shortcut: New shortcut to refresh tracking state, added to keyboard callbacks, toolbar tooltip, and all documentation
  • Consistent "Update" terminology: Replaced mixed "Save"/"Update" language across all user-facing text — tooltips, help text, docs, and README now consistently use "Update" for externalization operations (Ctrl+Shift+U, Ctrl+Alt+U)
  • Minimized UI fix: Reduced min_height from 72 to 66 to eliminate black bar at bottom of minimized manager (header 26px + toolbar 40px = 66px exactly). Increased min_width from 370 to 410 to accommodate the new button
  • Manager list default expand: Root-level items in the externalization list now start expanded on first launch instead of fully collapsed
  • Restored unit_tests annotations: 6 annotation groups accidentally removed in v5.0.269 commit (irony: the "fix annotation loss" commit) have been restored from git history
  • TDN reload rule: CLAUDE.md rule #1 strengthened — editing .tdn files on disk now mandates an immediate import_network MCP call to reload in TD
  • Manager toolbar docs: New toolbar button reference table added to manager-ui.md with all buttons, actions, and keyboard shortcuts

v5.0.275

TDN export keyboard shortcut pars, keyboard shortcuts documentation.

  • TDN export shortcut pars: Added Export Project to TDN and Export Current COMP to TDN read-only parameters to the UI custom page, displaying the ctrl/cmd + lshift + e and ctrl/cmd + alt + e shortcuts alongside the existing four shortcut pars
  • Keyboard shortcuts docs: Added an info callout to keyboard-shortcuts.md clearly explaining the difference between Save shortcuts (update tracked externalizations) and Export shortcuts (standalone TDN snapshot of any network)

v5.0.274

Settings persistence across upgrades, extension initialization timing documentation.

  • Settings persistence (.embody.json): Embody now saves user-configured parameters to a .embody.json file at the git root (or project folder if no git). Settings are written automatically on every parameter change and restored on project open (onStart) and fresh install (onCreate). Survives .tox upgrades, crashes, and force-quits. Whitelisted parameters include folder, Envoy config, tag names, tag colors, TDN settings, and logging options. Restore runs silently (no onValueChange side effects) via _restoring_settings flag
  • Crash-safe restore: _restoreSettings() runs at frame 5 on every project open, not just on fresh install. If the .toe has stale values (unsaved session, crash), .embody.json wins
  • Extension initialization timing docs: New documentation covering the critical onInitTD / TDN import timing issue — extensions inside TDN COMPs must defer initialization because ImportNetwork(clear_first=True) overwrites any state set during onInitTD. Added to td-python.md rule, create-extension skill, extensions.md doc, and TDN specification
  • Template sync: Updated text_rule_td_python.md and text_skill_create_extension.md templates to match their .claude/ counterparts

v5.0.269

Fix annotation loss on save, TDN v1.2, poisoned zero value guards, bridge improvements.

  • Fix TDN annotation loss on Ctrl+S: Two import-path bugs caused annotations to disappear after save. Phase 2 (_createCustomPars) called appendXXX(replace=True) on palette clone operators (annotateCOMP), destroying internal parameter bindings that the clone's rendering network depends on — fix: skip Phase 2 for palette_clone operators. Phase 1 (_createOps) only logged a warning when TD ignored the name param for annotateCOMP creates, causing Phase 7a to create duplicates — fix: explicitly rename after creation
  • Guard annotation import/export against poisoned zero values: Previous palette clone bug exported titleHeight=0, bodyFontSize=0, backAlpha=0.0 from broken annotations, making them invisible on reimport. Both import and export now skip zero values, letting palette clone defaults apply
  • TDN v1.2: Storage options, tdn_ref cross-validation, large TDN warning
  • Envoy bridge improvements: Signal diagnostics, startup log improvements
  • Toolbar/UI updates: Press state improvements, button interactions
  • New test coverage: test_tdn_file_io.py added for TDN file I/O operations

v5.0.263

DAT content safety, palette clone fidelity, recursive TDN fingerprinting, toolbar press states, venv validation.

  • DAT content safety: Pre-save check detects unexternalized DATs inside TDN COMPs that would lose content during the strip/restore cycle. Prompts with Externalize / Skip / Always Externalize / Never Ask options. New Tdndatsafety parameter stores the user's preference. Called from onProjectPreSave() before TDN export
  • Palette clone parameter fidelity: TDN export now compares parameters against both p.default and the clone source's actual value. Parameters that match p.default but differ from the clone source are preserved, fixing silent data loss on rebuild (e.g., buttontype defaulting to "momentary" when clone source is "toggledown"). clone/enablecloning parameters are excluded from export — TD auto-sets these
  • Recursive TDN fingerprinting: _computeTDNFingerprint() now recurses into child COMPs that don't have their own TDN externalization, so edits deep inside nested COMPs (e.g., editing a POP inside a geometryCOMP) trigger the parent's dirty detection
  • Toolbar and window header press states: Buttons now show a pressed visual on mousedown and restore hover on release, providing immediate click feedback
  • Manager list selection persistence: Selected row is tracked by operator path and survives list refreshes and reorders
  • Envoy venv validation: EnvoyExt now validates that the .venv Python actually executes before using it for the bridge. Catches stale pyvenv.cfg pointing to uninstalled TD versions and falls back to system Python with a warning
  • Bridge Python logging: Bridge now logs the Python executable path and version at startup for diagnostics
  • Envoyinstancename parameter removed: Auto-suffixed instance naming (MyProject, MyProject-2) is the sole mechanism. References removed from docs and skills
  • Documentation updates: New DAT Content Safety section in externalization docs, Broken Virtual Environment troubleshooting, expanded palette clone and fingerprint documentation in TDN specification, removed stale Envoyinstancename references across 5 docs
  • New tests: 12 palette clone round-trip fidelity tests (Section V in test_tdn_reconstruction.py). 39 test suites total

v5.0.260

Bridge stability: signal diagnostics, conditional bridge-script writes, connectivity wording fix.

  • Bridge signal diagnostics: envoy_bridge.py now installs SIGTERM/SIGINT handlers that log PID, current parent PID, and original parent PID before exiting. Startup log messages also include PID/PPID. Helps diagnose what process kills the bridge (Claude Code file watcher, orphan reaping, etc.)
  • Conditional bridge-script write: EnvoyExt._configureMCPClient() now compares bridge script content before writing. If unchanged, the file is not rewritten — preventing Claude Code's file watcher from restarting the MCP server mid-connection
  • Connectivity rule wording: Updated recovery step 3 from "close this tab/session and reopen a fresh one" to "reopen this session/conversation" for clarity

v5.0.259

Mandatory operator layout rules, /local path prohibition, TD connectivity recovery rule.

  • Mandatory operator positioning: The create-operator workflow now requires explicit set_op_position for every operator created via MCP. Auto-placement is no longer acceptable — agents must batch-compute grid-aligned positions before creating operators, verify layout afterward, and ensure left-to-right signal flow. Previously, positioning was documented as optional ("reposition if needed"), which led to messy, unreadable networks
  • /local path prohibition: New critical rule (#3 in CLAUDE.md) and step 1 in the create-operator workflow: agents must NEVER create operators under /local or /local/*. The /local storage is volatile and not saved with the .toe file. Agents must place operators under the project root or use ui.panes.current.owner.path to find the active network
  • TD connectivity recovery rule: New always-loaded rule (td-connectivity.md) with session-start verification, recovery procedures for lost MCP tools, and fix sequences for stale .envoy.json entries, stuck bridges, and dead TD instances

v5.0.258

Multi-instance Envoy support, auto-suffix collision avoidance, switch_instance bridge meta-tool.

  • Multi-instance port allocation: Envoy now scans a 10-port range (base through base+9) when the preferred port is occupied by another instance. Each TD instance gets its own port automatically — up to 10 simultaneous instances per base port
  • Instance registry collision avoidance: _instanceKey() now checks PID liveness before reusing a registry key. When the same .toe file is opened in multiple instances, keys are auto-suffixed (MyProject, MyProject-2, etc.). Stale entries with dead PIDs are reclaimed automatically
  • Envoyinstancename parameter: Optional custom name for the Envoy instance registry. Overrides the auto-generated key from the .toe filename — useful for predictable switch_instance targets
  • switch_instance bridge meta-tool: List all registered TD instances or switch the bridge to a different running instance. Redirects the bridge's HTTP target in-memory for instant switching with no restart
  • _findAvailablePort() refactor: Extracted port-scanning logic from Start() into a dedicated method. Replaces the recursive retry loop with a clean single-pass scan
  • Atomic JSON writes: New _atomicWriteJSON() method for .envoy.json writes — uses temp file + os.replace() with Windows PermissionError retry to prevent corruption under concurrent access
  • Graceful shutdown via MCP: Documented project.quit() as the preferred way to close TD instances programmatically — triggers onDestroyTD for clean deregistration
  • Multi-instance documentation: New /multi-instance skill, updated architecture docs, setup guide, Claude Code integration docs, troubleshooting entries, and tools reference. All surfaces document switch_instance, port allocation, instance registry, and same-project behavior

v5.0.252

Windows process-kill fix, reconstruction verification fix.

  • Windows is_process_alive() fix: os.kill(pid, 0) on Windows calls TerminateProcess(), killing TouchDesigner instead of checking liveness. Every get_td_status, launch_td, and restart_td call terminated TD on Windows. Now uses OpenProcess(SYNCHRONIZE) via ctypes on Windows, preserving the Unix signal-0 path for macOS/Linux
  • Reconstruction verification fix: _verifyReconstructedComp() accessed child.errors and child.warnings as properties instead of calling them as methods (child.errors(), child.warnings()). This caused 'builtin_function_or_method' object has no attribute 'split' warnings on every TDN reconstruction — error and warning checking was silently skipped
  • New tests: 2 Windows is_process_alive tests (mocked OpenProcess for live and dead PIDs). 39 test suites total

v5.0.251

Nested TDN child-skip on import, depth-sorted reconstruction ordering, material reference fix.

  • Nested TDN child-skip during import: When a parent TDN contains children for a child COMP that has its own TDN externalization entry, the child's children array is now skipped during import. The child COMP shell is still created, but its internal network is left to its own .tdn file — preventing stale parent snapshots from overwriting updated child networks. New _getTDNExternalizedPaths() and _stripNestedTDNChildren() helper methods handle detection and recursive stripping
  • Depth-sorted TDN reconstruction: _getTDNStrategyComps() now sorts entries by path depth (fewest segments first), ensuring parents are always imported before their children during project-open reconstruction. Combined with the child-skip logic, each COMP's network is populated exactly once from its authoritative .tdn file
  • Import input validation: ImportNetwork() now validates that operators is a list, returning a clear error instead of failing cryptically on malformed input
  • Material reference test fix: Corrected test_T07_geometry_material_roundtrip to use ./my_mat (child reference) instead of my_mat (sibling reference), which was unresolvable from inside the geometryCOMP
  • assertAlmostEqual added to test framework: TestRunnerExt now supports assertAlmostEqual(first, second, places=7, delta=None) for floating-point comparisons
  • TDN spec updated: New "Nested TDN-Externalized COMPs" section documents the child-skip behavior, import/export semantics, and reconstruction ordering
  • Externalizations table cleanup: Removed stale test entries (tdn_geo_test, tdn_deep, etc.) from tracking table
  • New tests: 4 nested TDN child-skip tests (Section U: skip children of TDN-externalized COMPs, import non-TDN children normally, depth sorting verification, deeply nested skip). 39 test suites total

v5.0.247

Default-child cleanup on TDN import, nested TDN save-cycle fix, SOP-to-COMP connection hardening.

  • Clear auto-created defaults on COMP creation during import: When TDN import creates a COMP (e.g. geometryCOMP) that has inline children defined, auto-created default children (e.g. Torus POP) are now destroyed before recursing into the TDN children. Previously, default children persisted alongside imported ones because they were filtered out during export (_TRIVIAL_KEYS) and never visited during import. Verified at 10 levels of nesting depth
  • Nested TDN strip/restore ordering: Save cycle now strips deepest-first and restores shallowest-first. Previously, stripping a parent TDN COMP destroyed nested TDN COMPs before they could be tracked, so post-save restore never rebuilt them — leaving default children instead of the correct TDN contents
  • SOP-to-COMP connection fallback: _wireConnectionList now bounds-checks inputConnectors before indexing and falls back to inputCOMPConnectors for COMPs that accept SOP/TOP/CHOP wire inputs where connectors may not be populated immediately after creation

v5.0.243

Headless smoke testing, file cleanup preferences, specialized COMP support, portable .tox hardening, bridge project_path override.

  • _messageBox auto-response system: Dialog calls can be intercepted by seeding _smoke_test_responses in storage, enabling fully headless smoke testing of Embody's init sequence including Envoy opt-in and re-scan prompts. Responses are consumed on use
  • File cleanup preference: New Filecleanup parameter (ask/keep/delete) controls whether external files are deleted when un-tagging operators. "Always Keep" and "Always Delete" options persist the choice
  • TDN default child filtering: Uncustomized auto-created children (e.g. torus1 inside a geometryCOMP) are now skipped during export — they carry only trivial keys (name, type, position, size) and TD recreates them on COMP creation
  • Portable .tox export hardening: ExportPortableTox now strips the target COMP's own externaltox/enableexternaltox params (not just descendants) and handles the syncfile parameter, preventing baked-in references from confusing recipients
  • Bridge project_path override: launch_td and restart_td meta-tools accept an optional project_path parameter to open a different .toe file, resolved relative to the git root
  • Envoy start deferred: parexec.py defers Start() by 5 frames so onCreate has time to suppress baked-in Envoyenable=True before the server launches
  • SCM directory protection: deleteEmptyDirectories and _cleanupFolder now skip .git, .svn, and .hg directories
  • Cross-platform temp paths: All Envoy temp file operations use tempfile.gettempdir() instead of hardcoded /tmp
  • findChildren() fix: Two calls using invalid depth=-1 corrected to findChildren() (unlimited depth is the default)
  • AGENTS.md rewrite: Condensed from verbose rule duplication into a concise universal AI instructions file
  • ENVOY.md updated: TDN-first rule added, skill prerequisites section, verify-TD-claims rule
  • Release smoke test infrastructure: Bootstrap script (smoke_bootstrap.py) and template .toe for E2E release testing
  • New tests: 22 smoke release tests (post-init state, _messageBox mechanism, _promptEnvoy auto-response, Envoy state), 9 specialized COMP roundtrip tests (geometryCOMP children, flags, materials, strip/restore; cameraCOMP; lightCOMP). 39 test suites total

v5.0.237

TDN v1.1 format with target COMP metadata, import error surfacing, MCP permissions documentation, save-cycle pane restoration, git init error dialog, Envoy troubleshooting docs.

  • TDN v1.1 format: Exports now include the target COMP's type, flags, color, tags, comment, and storage at the top level. On import, type mismatches produce a warning. Existing v1.0 files remain fully importable
  • Locked non-DAT operator warning: Export and import now detect locked TOPs, CHOPs, and SOPs and warn that their frozen data won't survive a TDN round-trip. Documented in spec and externalization docs
  • Import error surfacing: ImportNetwork() and ImportNetworkFromFile() now set ui.status on failure, so TD users see errors in the status bar — not just in logs or MCP responses
  • MCP auto-authorization documented: The Envoy enable dialog now informs users that all MCP tools are auto-authorized and points to .claude/settings.local.json for adjustments. New "MCP Tool Permissions" section added to Envoy setup docs
  • Save-cycle pane restoration: When TDN strip/restore runs during project save, pane owners inside TDN COMPs are now saved before stripping and restored after import — no more orphaned panes
  • Git init error dialog: If git init fails during Envoy setup, a ui.messageBox now shows the error and manual fix instructions instead of silently falling through
  • Envoy troubleshooting docs: New troubleshooting page covering server startup failures, connection issues, git init problems, and log file locations
  • Dialog sequencing fix: The Envoy opt-in prompt now waits for all other init dialogs (deprecated patterns, re-scan) to resolve before appearing
  • TDN reconstruction uses type from file: ReconstructTDNComps() now reads the type field from v1.1 .tdn files when creating missing COMP shells, so the correct COMP type (geometryCOMP, containerCOMP, etc.) is used instead of defaulting to baseCOMP
  • New tests: Locked non-DAT warning test, target COMP metadata preservation tests (6 tests for type, flags, color, tags, comment, storage round-trips)

v5.0.235

restart_td bridge meta-tool, local MCP handshake when TD is down, operator overlap warnings, layout rules hardening.

  • restart_td bridge meta-tool: Gracefully quits TouchDesigner and relaunches with the project's .toe file. Sends platform-appropriate quit signal, waits for exit (force-kills if needed), then relaunches and waits for Envoy. Crash-loop aware — respects the existing 3-in-5-minutes limit
  • Local MCP handshake when TD is down: The STDIO bridge now handles initialize, notifications/initialized, and tools/list locally when Envoy is unreachable, so Claude Code always completes the MCP setup and discovers bridge meta-tools without waiting for a connection timeout
  • set_op_position overlap warning: After repositioning an operator, EnvoyExt checks for bounding-box overlaps with siblings (20-unit margin) and returns an overlap_warning field naming the conflicting operators
  • Layout rules hardening: Network-layout and create-operator rules now require dimension-aware spacing (nodeWidth/nodeHeight from get_network_layout), forward-flow wire direction, and flag the fixed-offset anti-pattern. OP-reference parameter values section added to parameter rules
  • Bridge meta-tools documented: Architecture, setup, claude-code, and tools-reference docs updated with STDIO bridge section, .envoy.json config reference, and meta-tool catalog

v5.0.233

Project-level performance monitoring, pre-handoff validation, Envoy bridge hardening, test runner dialog fix.

  • get_project_performance MCP tool: Reads a permanent Perform CHOP inside Embody to report FPS, frame time, GPU/CPU memory, dropped frames, active ops, GPU temperature, and optional COMP hotspot ranking by cook time
  • /validate command: Pre-handoff checklist that snapshots performance, scans for errors, checks externalization health, evaluates thresholds, and reports a PASS/WARN/FAIL verdict with hotspot analysis
  • Test runner dialog fix: Filecleanup parameter is now suppressed to delete during test runs (save/restore across all entry points), preventing modal "Removed Operator Detected" dialogs from blocking test execution
  • Continuity check sandbox filtering: Path-based filtering for test sandbox operators as a second safety layer — sandbox ops are silently filtered even when the _running flag isn't active (handles reinit, between-suite gaps, post-failure)
  • Envoy bridge hardening: .envoy.json project config for bridge launcher, venv Python preference over system Python, stale process cleanup with orphan watchdog

v5.0.229

Warning support in get_op_errors, Envoy enable dialog improvement, cleanup.

  • get_op_errors now returns warnings: The MCP tool calls both OP.errors() and OP.warnings(), returning structured warnings/warningCount/hasWarnings fields alongside existing error data. Cook dependency loops and other TD warnings are now surfaced to AI clients
  • Envoy enable dialog note: The first-run dialog now mentions that TD will be briefly unresponsive during dependency installation
  • Cleanup: Removed stale base_tox.tdn and test externalization entries from tracking table

v5.0.228

macOS timezone fix, toolbar hover highlight.

  • macOS timezone abbreviation fix: Local timestamp display in the manager list now shortens verbose macOS timezone names (e.g. "Pacific Daylight Time" → "PDT") by extracting initials
  • Toolbar hover highlight: Container right toolbar button background color now uses an expression to brighten on hover

v5.0.227

TDN crash safety, atomic writes, content-equal skip, About page filtering.

  • Atomic TDN writes: TDNExt._safe_write_tdn now writes via temp file + os.replace + fsync to prevent partial writes corrupting .tdn files on crash or power loss
  • Backup rotation: Before each write, .tdn files are copied to .tdn_backup/ (.bak and .bak2 generations). .tdn_backup/ is git-ignored
  • Post-write validation: After each atomic write, the file is read back and parsed. If validation fails, the previous backup is automatically restored
  • Rollback on reconstruction failure: ReconstructTDNComps and onProjectPostSave now attempt rollback from .bak if reconstruction fails after import
  • Content-equal skip: Pre-save export compares new TDN content against the existing file (ignoring volatile header fields: build, generator, td_build, exported_at). Unchanged COMPs are skipped, eliminating noisy git diffs
  • Structural dirty detection: Refresh now detects structural changes in TDN-strategy COMPs (not just parameter changes) and triggers SaveTDN when children are added/removed/renamed
  • About page filtering: Build, Date, and Touchbuild parameters are excluded from TDN export and reconstructed from externalizations.tsv at import time via _reconstructAboutPage. Prevents version metadata from polluting TDN diffs
  • Continuity dialog suppression: File cleanup dialog is suppressed when the test runner is active, preventing modal spam during rapid operator create/destroy cycles
  • Continuity check fix: Individually-externalized children are only skipped if the parent TDN COMP is completely absent (crash recovery). If the parent exists but is empty, genuine deletions are detected normally
  • Rules frontmatter strip: _writeTemplate now strips YAML frontmatter before writing rules to user projects (Claude Code doesn't read frontmatter in .claude/rules/)
  • New test suite: test_tdn_crash_safety.py — atomic write behavior, backup rotation, post-write validation, failure injection, and stress tests (37 total suites)
  • Expanded test coverage: test_tdn_helpers.py adds _tdn_content_equal and _read_existing_tdn tests; test_tdn_reconstruction.py adds S-series About page filtering tests

v5.0.222

Rename tag_for_externalization to externalize_op, clarify single-step workflow.

  • MCP tool rename: tag_for_externalizationexternalize_op across EnvoyExt, docs, skills, templates, and settings. The new name better reflects that the tool tags AND writes to disk in one step
  • Externalize workflow clarification: Skill and docs now explain that externalize_op is a single-step operation (no separate save_externalization needed), and that save_externalization is for re-exporting already-externalized operators
  • Test updates: Renamed test methods and references to match new tool name

v5.0.221

TDN annotation properties, GitHub release rule, templates cleanup.

  • TDN annotation properties: Export and import now support backAlpha, titleHeight, and bodyFontSize annotation parameters, preserving non-default values through TDN round-trips
  • GitHub release rule: New .claude/rules/github-release.md with post-push workflow for detecting release artifacts, extracting version from changelog, and creating GitHub releases via gh CLI. Added to EmbodyExt._TEMPLATE_MAP_RULES for auto-deployment, template synced, release-commits.md updated with mapping
  • Templates TDN cleanup: Annotations in templates.tdn now use the native annotation format instead of being represented as annotateCOMP operators. Removed annotateCOMP type defaults and par_templates section. Expanded Rule Templates annotation to accommodate new template DAT

v5.0.220

Network layout rule rewrite, commit-push checklist, expanded settings template, tooltip fix.

  • Network layout rule rewrite: Replaced verbose placement rules with a concise 7-step placement procedure, added anti-patterns section and complexity thresholds for when to encapsulate into COMPs. Template synced
  • Commit-push checklist rule: New .claude/rules/commit-push-checklist.md enforcing change evaluation, doc audit, test audit, and release detection before every commit. Added to EmbodyExt._TEMPLATE_MAP_RULES for auto-deployment, template synced, release-commits.md updated with mapping
  • Expanded MCP tool allowlist: Settings template (text_settings_local.json) now includes all 42 MCP tools sorted alphabetically, instead of only read-only tools
  • Tooltip fix: Toolbar tooltip text changed from "Refresh tracking state" to "Clear filter" with repositioned widget
  • Parameters template BOM fix: Restored missing BOM marker on text_rule_parameters.md

v5.0.217

TDN target COMP parameter preservation, user-prompted file cleanup, dock safety, companion reuse fix, git init hardening.

  • Target COMP parameter preservation: TDN export now captures the target COMP's own custom parameters (custom_pars) and non-default built-in parameters (parameters) at the root level of the TDN document. Import restores these in a new Phase 9 after child creation, so extension reinit doesn't clobber custom par values. 5 new tests cover roundtrip survival of custom pars, expressions, built-in params, bare shell creation, and backward compatibility
  • Help text in TDN: Custom parameter definitions now export and import help tooltip text. TDN schema and specification updated with the new help field
  • User-prompted file cleanup: When the continuity check detects externalized operators removed from the network whose backing files still exist on disk, Embody now prompts the user to keep or delete the files instead of silently skipping. Supports "Always Keep" / "Always Delete" persistent preferences via the new Filecleanup parameter
  • Dock safety on destroy: Both _clearChildren (EmbodyExt) and TDN import (clear_first) now clear child.dock = None before destroying child operators, preventing uncatchable tdError when a dock target is destroyed before its docked operator
  • Companion reuse fix: _createOps now tracks pre-existing operator names to distinguish them from auto-created companions during merge imports. Prevents merge (non-clear_first) imports from incorrectly reusing operators that existed before import started
  • Git init hardening: _ensureGitRepo strips GIT_DIR, GIT_WORK_TREE, and other git env vars before git init to prevent broken repos caused by TD's embedded Python environment. Verifies the init with git rev-parse and retries on failure
  • attrs<25 version pin: MCP dependency install now pins attrs<25 to avoid conflicts with TD's bundled attr module. Startup detects and downgrades attrs 25.x automatically
  • Tagger refactoring: Extracted _removeExternalization (no-dialog removal) and _dispatchTaggerButton (label-based routing for manage-mode buttons) from inline handler code
  • RemoveListerRow / _removeTDNStrategy: Now accept delete_file parameter to optionally preserve files on disk when removing tracking entries
  • Parameter rules: New dedicated .claude/rules/parameters.md covering help text, sections, naming, ranges, styles, and page organization. td-python.md now points to it. TD API reference skill updated with post-creation property examples
  • New tests: test_strategy_handlers.py with 15 tests covering _removeExternalization, HandleStrategySwitch, _dispatchTaggerButton, and manage-mode button dispatch. test_mcp_externalization.py gains tearDown cleanup for sandbox entries

v5.0.210

DAT restoration on startup, continuity check hardening, manager list row limiting.

  • Automatic DAT restoration: New RestoreDATs() method recreates missing DAT-strategy operators from externalized files on project open (frame 50). Controlled by Datrestoreonstart parameter. Safely excludes Embody descendants and DATs inside TOX/TDN COMPs
  • Continuity check hardening: Before removing entries for missing operators, checks if the backing file exists on disk — recoverable entries are preserved for restoration instead of being deleted
  • Manager list row limiting: Tree starts collapsed by default. LRU-based auto-collapse keeps visible rows under 100, protecting the active branch from being collapsed
  • TDN structural cleanup: toolbar.tdn and tagger.tdn shed embedded child definitions in favor of externalized .tdn files — smaller diffs, cleaner hierarchy
  • base_test converted to TDN: Replaced binary base_test.tox with base_test.tdn for diffability
  • text_claude.md relocated: Template moved from Embody/ root into Embody/templates/ alongside other template DATs
  • New tests: test_dat_restoration.py with 13 tests covering DAT restoration, skip conditions, and continuity check recovery

v5.0.208

Settings auto-deploy, bridge template, Envoy startup resilience.

  • settings.local.json auto-deploy: Read-only MCP tool permissions deployed automatically on Envoy startup
  • Bridge script template: text_envoy_bridge.py and settings template moved into the templates COMP for centralized management
  • Envoy startup resilience: _upgradeEnvoy() failure no longer blocks MCP server startup
  • .gitignore managed entries: Expanded auto-managed entries to include Backup/, logs/, CrashAutoSave*

v5.0.207

Claude Code integration docs, slash commands, CLAUDE.md deduplication.

  • Claude Code Integration docs: New documentation page covering the generated .claude/ directory — rules, skills, slash commands, and customization
  • Slash commands: Added /run-tests, /status, and /explore-network commands to .claude/commands/ for common workflows
  • CLAUDE.md deduplication: Moved rules that were restated in both CLAUDE.md and .claude/rules/ into rules only — reduced critical rules from 15 to 9. Skill prerequisites moved to dedicated skill-prerequisites.md rule
  • Getting Started update: .gitignore documentation updated to reflect specific .claude/ entries instead of blanket directory exclusion

v5.0.206

Metadata reconciliation, network layout tool, save_externalization fix.

  • Metadata reconciliation: New ReconcileMetadata() method runs at frame 75 on project open — re-applies tags, colors, file parameters, and readOnly flags to operators that exist in the externalizations table but lost their in-memory metadata (e.g. when TD was closed without saving after tagging)
  • get_network_layout MCP tool: Returns positions and sizes of all operators and annotations in a COMP in a single call — replaces the need for repeated get_op_position calls. Includes bounding box calculation
  • save_externalization fix: Now correctly handles TDN-strategy COMPs (calls SaveTDN) and file-synced DATs, instead of blindly calling Save() which only works for TOX-strategy COMPs
  • Save() guard: Validates target is a COMP before proceeding — prevents cryptic errors on non-COMP operators

v5.0.205

Fix companion DAT duplication during TDN strip/restore save cycle.

  • Companion DAT reuse on import: _createOps now detects auto-created companion DATs (timerCHOP callbacks, rampTOP keys, etc.) and reuses them instead of creating duplicates that accumulate on each save
  • Duplicate companion cleanup on export: _exportChildren detects and skips accumulated companion duplicates (e.g. timer1_callbacks1, timer1_callbacks2) using docking-based detection — existing .tdn files self-clean on next save

v5.0.204

Custom window header, path portability, TDN template cleanup.

  • Custom window header: Replaced widgetCOMP clone of TDBasicWidgets with a lightweight containerCOMP + WindowHeaderExt extension — minimize/maximize/close with hover-based button detection, no palette dependency
  • Absolute path elimination: Replaced hardcoded /embody/... paths with relative expressions (=op('container_left'), =me.op('externalizations'), etc.) in toolbar and root TDN files for full portability
  • TDN template cleanup: Removed unused type_defaults entries (baseCOMP, panelexecuteDAT, constantTOP, opexecuteDAT) and stale custom par pages (settings_2, expressions) from Embody.tdn — smaller, cleaner exports
  • EmbodyExt.py: self.ownerComp.pathself.my.path for consistency with codebase conventions

v5.0.203

Multi-client AI config, TDN docking, robust init.

v5.0.201

Robust first-install init, table schema expansion, release build hardening.

  • Automatic init on drop: onCreate() now disables Envoy before the table exists (prevents premature git-root detection), creates the externalizations table at frame 15, then runs Verify() at frame 30 — all fully async and idempotent
  • CreateExternalizationsTable() (new public method): Safe to call at any time. No-op if the table is already connected; reconnects to a surviving sibling after an upgrade without duplicating; creates fresh only when truly absent. Also wired to the Create Externalizations Table pulse parameter
  • Verify() — two-scenario detection: Fresh install (empty table) runs UpdateHandler quietly and offers Envoy opt-in. Upgrade (table has prior data) prompts a re-scan dialog before offering Envoy opt-in
  • Externalizations table schema: Added strategy, node_x, node_y, and node_color columns. Existing tables are migrated automatically on first open
  • Release build: execute_src_ctrl.py now clears Tdnfile and Networkpath pars before ExportPortableTox() so the baked .tox doesn't carry stale TDN paths into new projects

v5.0.190

Automatic restoration, documentation overhaul.

  • Automatic restoration: TOX-strategy COMPs are restored from .tox files and TDN-strategy COMPs are reconstructed from .tdn files on project open — users no longer need to save their .toe to preserve externalized work
  • Documentation overhaul: Updated all documentation (README, docs site, help text, CLAUDE.md, text_claude.md) to reflect that externalized files on disk are the source of truth, removing outdated ctrl+s save workflow references

v5.0.178

Reload from disk, full project TDN safety, continuity hardening.

v5.0.171

Export Portable Tox, improved tag management, TDN error handling, window management refactor.

  • Export Portable Tox: New ExportPortableTox() method exports any COMP as a self-contained .tox with all external file references and Embody tags stripped. Available from the Manager UI Actions menu and used automatically for release builds
  • Improved tag stripping: Disable now sweeps all project operators for stale Embody tags, not just tracked ones
  • TDN error handling: ImportNetworkFromFile now returns structured error dicts instead of None on failure
  • TDN per-COMP split: Refactored _splitPerComp into a reusable static method
  • Window management: Tagging menu and manager UI refactored into standalone window COMPs (window_tagging_menu, window_manager)
  • Keyboard shortcut update: lctrl-lctrl now shows an Actions menu for already-tagged operators (tag, retag, export portable tox, etc.)
  • Release build: execute_src_ctrl.py now uses ExportPortableTox() instead of raw comp.save() for portable release .tox files
  • Test fixes: Updated test_custom_parameters (synchronous ReexportAllTDNs call, Envoy transitional state handling) and test_tdn_reconstruction (improved continuity check distinguishing pure TDN children from individually-externalized ones)

v5.0.163

Re-export TDN files for list, manager, and container_right after param changes.

v5.0.140

TDN strip/restore hardening, file/syncfile export, post-import validation, TDN restore UI, companion DAT reuse during import, bug fixes.

  • Save-in-progress guard blocks mutating MCP operations during the strip/restore save window
  • Pre-save verifies .tdn file exists before stripping children (prevents data loss)
  • Post-save tracks restore failures for retry on next project open
  • file and syncfile parameters now exported in TDN for self-contained externalized DAT round-trips
  • New "Restore from TDN" button in tagger actions menu for TDN-strategy COMPs
  • Post-import validation checks for missing file references and cook errors
  • Companion DATs (auto-created by rampTOP, timerCHOP, etc.) are reused during import instead of creating duplicates
  • Component-level TDN files protected from stale-file cleanup during project-level exports
  • StripCompChildren now destroys annotations and respects Embody protection chain
  • UI: midline ellipsis glyph for strategy column, active-menu state tracking, "Tag" label for unexternalized COMPs
  • Fixed: SaveDAT() crash (undefined property), _save_externalization type mismatch, duplicate row corruption (missing strategy column)

v5.0.130

TDN strategy externalization, strip/restore save cycle, compact TDN format.

  • New externalization strategy: COMPs can use TDN (JSON export/import) instead of TOX, enabling human-readable diffs
  • Strip/restore save cycle: TDN-strategy COMP children are stripped before .toe save and reconstructed from .tdn on project open, keeping the .toe small
  • Compact TDN format: type_defaults hoists shared parameter values, par_templates deduplicates custom parameter definitions, expression shorthand (= prefix for expressions, ~ for binds)
  • Per-COMP split export mode: large networks export as one .tdn file per COMP for git-friendly directory structures
  • externalizations.tsv gains strategy column (tox, tdn, py, txt, etc.)
  • Continuity check skips TDN-strategy children (lifecycle managed by TDN, not individual externalization)
  • 30 test suites covering all functionality

v5.0.93

Modular sub-components, TDN snapshots, README rewrite.

  • Embody UI refactored into externalized sub-components (toolbar, tagger, manager, window manager)
  • TDN network snapshot support added
  • README comprehensively rewritten with full feature documentation

v5.0.86

Manager UI refactored into modular externalized components.

v5.0.71

Rename Claudius to Envoy, expand README and help text.

v5.0.61

Rename MCP tools for consistency, add auto-restart on port change, expand testing documentation.

v5.0.59

Migrate tests to externalized DATs, add deferred test runner (one test per frame).

v5.0.56

Rewrite test runner, fix run() safety, add 6 new test suites, update documentation.

v5.0

Major release — Envoy MCP server, TDN format, comprehensive testing.

  • Envoy MCP Server: 40+ tools for Claude Code integration
  • TDN Format: JSON export/import for operator networks
  • Test Framework: 26 test suites with sandbox isolation
  • Structured Logging: Multi-destination logging system
  • CLAUDE.md Auto-Generation: Project context for AI assistants
  • Cross-platform: macOS support

v4.7.14

Safe file deletion — Embody now only deletes files it created. Untracked files preserved during disable/migration.

v4.7.11

Cross-platform path handling (forward slashes on all platforms) + code cleanup.

v4.7.6

Build save increment bug fix.

v4.7.5

  • ui.rolloverOp refactor
  • Restore handling of drag-and-drop COMP auto-populated externaltox pars
  • Cache parameters correctly between tox saves
  • Parameter updated coloring for dirty buttons in UI
  • Path lib implementation improvements
  • Auto refresh on UI maximize
  • Ignore untagged COMPs when checking for duplicate paths

v4.6.4

  • About page on externalized COMPs (Build Number, Touch Build, Build Date)
  • Build/Touch Build in externalization table + Lister
  • Window resizing support

v4.5.23

  • Fix deletion of old file storage after renaming
  • Network cleanup, tagging optimization
  • Fix duplicated rows from git merge conflicts

v4.5.19

Allow master clones with clone pars to be externalized. Setup menu cleanup.

v4.5.17

Bug fixes, smaller minimized window footprint.

v4.5.2

  • TSV support
  • Clone tag for shared external paths
  • Handle drag-and-dropped COMP externaltox pars
  • Detect dirty COMP parameter changes

v4.4.128

Support for COMPs with empty/error-prone clone expressions.

v4.4.127

Textport warning for paused timeline.

v4.4.126

Clean up Save and dirtyHandler methods, auto set enableexternaltox.

v4.4.104

TreeLister, improved Tagger stability, color theme updates.

v4.4.74

  • Full project externalization
  • Handle deletion and re-creation (redo) of COMPs/DATs
  • Support renaming and moving COMPs/DATs

v4.3.128

Fixed abs path bug, macOS Finder support, keyboard shortcuts.

v4.3.122

Separated logic/data for easier Embody updates.

v4.3.43

UTC timestamps, Save/Table DAT buttons, refactored tagging.

v4.2.101

Fixed keyboard shortcut bug, updated to TouchDesigner 2023.

v4.0.0

Support for various file formats, parameter improvements.

v3.0.0

Initial release.