Changelog¶
v6.0.222¶
A ten-lens adversarial review of v6.0.219/220 found a data-loss defect in the release itself. This fixes it, plus the two loose ends that review turned from suspicions into specifics.
- An unreadable job record is never deleted again -- it is quarantined, and only when corruption is PROVEN: v6.0.219 taught the reaper to clear a delivery record it could not parse, so one truncated file could no longer freeze every node-cleanup sweep on the host. But the store answers "None" to two different questions -- this file is torn and I could not open this file right now -- and the new branch could not tell them apart. A momentary lock (an antivirus or backup handle, file-descriptor exhaustion, or the atomic rename of a concurrent writer, since that pass holds no lock) therefore destroyed a perfectly good record. Worse, the branch ran above both of the reaper's safety gates, so it also took the one class this store must never lose: an unacknowledged
indeterminate, the sole durable proof that a command may have executed. Reproduced with a transient error, then fixed. Corruption must now be proven -- the bytes read successfully and the parser refused them, after retries -- and a proven-corrupt record is renamed aside rather than deleted, so it stops blocking cleanup while its bytes stay on disk. Bounded per pass, so no single sweep can empty the queue. - Deferred: flushing every Convoy App write to disk before its publishing rename. It shipped briefly in v6.0.222 and is withdrawn here. The writer behind
host.json, every delivery record,policy.jsonand the rest does a temp-write plus atomic rename with no flush, so the rename is atomic for the name while the contents may still be in the operating system's cache -- a real gap, and three sibling modules already close it. But syncing there also made every write about three times slower and held the destination handle open longer, and a mass revocation on Windows then began exhausting the sharing-violation retries that the same writer uses to survive a concurrent reader: 250 queued jobs, 7 transitioned, 7 errors, on CI only. Trading a rare durability risk for a reproducible availability one is the wrong trade, so this returns as its own change -- with the retry loop and the handle's lifetime reworked together. - Two ways the Convoy reconciler could stop, silently, until TouchDesigner restarted: accelerating the loop published its new generation before arming the replacement tick, so if arming failed the already-armed tick stood down against a generation nothing carried -- and the failure was swallowed without a word at any log level. It now arms first and commits second, and says so if it cannot. Separately, the recovery written to unstick a wedged host slot had exactly one caller: a user pressing a button. The reconcile loop -- the one thing that runs continuously, and the one that actually starves when the slot wedges -- could not reach it, so a wedged flag stopped registration, heartbeats and the node list indefinitely while the cure sat unreachable.
- The button's own words match what it now does: the parameter help and the log line both still described the pre-6.0.219 contract ("nodes with unresolved jobs are kept") -- the log line word for word the sentence from the original field report. Both now say what the code enforces: a node is kept only while a delivery has not finished, because a finished result is fetched by delivery id and outlives the row. The Convoy landing page too.
- Tests that can actually fail. Mutation testing showed the headline v6.0.219 fix was pinned on only one of its three paths: reverting the guard on Forget Offline Nodes, or on the automatic eviction sweep, left the entire daemon suite green -- and Forget Offline Nodes is the exact path that produced the reported symptom. Both now have tests, each verified to die when the fix is reverted. Four new tests in all, including one that proves a merely-locked record survives a reaping pass.
v6.0.220¶
Upgrading Embody can now update the Convoy App without restarting TouchDesigner.
- An upgrade gets its own daemon-update attempt: the Convoy App is the process where every node-cleanup sweep actually runs, and since v6.0.213 a TouchDesigner running newer code updates an older daemon in place automatically. That check is deliberately budgeted -- one attempt per TD session -- and both of its guards live in a session store that survives an extension reinit and the Embody COMP being replaced, dying only with the TouchDesigner process. Upgrading Embody does neither: the self-updater swaps the COMP in place, and a drag-and-drop replacement lands at the same path. So the marker recorded before an upgrade ("this daemon's version needs nothing") kept matching the daemon's unchanged report afterwards, and the check was skipped for the rest of the session -- new Embody, old daemon, none of the fixes it was upgraded for, and nothing on screen saying why. The budget is now keyed on the Embody version as well as the session, so an upgrade re-arms exactly one attempt. A failed install still does not retry until the next upgrade or restart, which is what the budget existed to guarantee.
- Two new tests: the upgrade-in-a-live-session sequence end to end, and a pin that re-arming does not re-open the retry loop. The version is injected rather than read from the live parameter -- the first draft of that test wrote
9.9.9into the running project's Version and would have baked a garbage release on the next save.
Verified on a real deployment rather than in a harness: a fresh install of this build, with Convoy enabled, took the machine's daemon from 6.0.212 to 6.0.220 on its own -- graceful stop, new payload, restart, no button pressed.
v6.0.219¶
Duplicate node rows clear themselves -- the guard that made them permanent is gone.
- A finished result no longer pins a node row (duplicate root cause #6): one predicate decided whether a node "still had work", and it answered yes for two very different things -- a delivery that had not finished, and a delivery that had finished but whose result nobody had collected. Only the first needs its node. A result is fetched by its delivery id and outlives the row entirely; forgetting a row deletes no job record at all. Because that predicate gated all three cleanup paths -- the supersede sweep that collapses versioned-save duplicates, the Forget Offline Nodes button, and the automatic stale-row eviction -- a single uncollected result made a duplicate row permanent, against every mechanism built to remove it. A field machine had eleven rows across two projects and 123 job records, 115 of them exactly this class; "Forget Offline Nodes" answered "forgot 0, kept 8 with unresolved jobs" and the rows came straight back. Unfinished work still keeps its row; a finished one no longer does, anywhere.
- A queued delivery on a superseded identity is resolved, not left to haunt it: a delivery still sitting in the queue when its node is superseded provably never left this host, so it is now durably refused with the host's own evidence naming the successor -- terminal, collectable and reapable -- instead of pinning the row forever (nothing could cancel it: the drain had no node to route it to, the reaper only removes terminal records, and acknowledgement refuses a non-terminal one). Work that has crossed the dispatch boundary is untouched, always: that verdict belongs to the node, never to the host. Work that would still run without its row --
convoy_git,convoy_gh,convoy_shell,convoy_ping,convoy_start_node, all of which execute on the host -- spares the row and simply finishes, which the daemon now decides with the dispatcher's own rule rather than a second copy of it. - A crashed session's row can be collapsed too: the "never retire a row that can still answer" spare keyed on the Envoy port alone, so a session that died without a clean exit left a port nobody could clear and a row nothing could touch. A live server heartbeats, so the spare now also requires that the row has been heard from within the dead-node grace window -- read from the durable last-seen stamp, never a socket probe (a closed-port refusal costs ~2 s on Windows, and this runs inline on every registration).
- Forget Offline Nodes stops reporting a refusal as a success: a run that forgot nothing logged green, because the result was graded on "did anything error" rather than on what actually happened. It is now graded on the outcome, a keep is reported on screen naming the nodes kept and the deliveries pinning them, and any row the daemon declined is put back into the list in the same frame instead of silently reappearing two or three seconds later. The daemon's own explanation is carried through to that dialog, including from an older daemon that sends only prose.
- The refusal says which deliveries pin the row:
/nodes/forgetnow returns the blocking delivery ids and an exact pending count in the response body, so the work can be found and cancelled. Previously the daemon knew precisely which deliveries were responsible and reported only a de-duplicated list of state words. - An unreadable job record no longer freezes cleanup forever: a record nobody can parse -- a crash mid-write -- must stop a deletion, because it might be the pending delivery for the row being retired. It now says so instead of failing silently (the ids are audited, and the refusal names them), and the reaper removes one past its retention window, so the freeze is bounded rather than permanent. The old predicate claimed to fail closed here and actually failed open, deleting on a store it could not read.
- Two latent ghost-resurrection paths closed: deleting a node row committed the change in memory before writing it to disk, so a failed write left the row gone from memory, still on disk, and a retry -- finding nothing left to delete -- wrote nothing and reported success; the next daemon start replayed the ghost. Forget Offline Nodes had the same inversion in the opposite order. Both now persist before committing, matching the rule the rest of the file already documented. Retiring a row also drops its TouchDesigner-Python approval, which every automatic retirement had been leaving behind.
- Performance: the work census reads the jobs directory once per sweep, and only once a duplicate actually exists -- the old predicate re-parsed the entire directory once per candidate row, under the app lock, on every heartbeat (eight duplicate rows meant eight full scans a minute; a directory of 5,000 records measured ~0.5 s per pass).
- Twenty-one new tests, and two that pinned the old behaviour rewritten rather than deleted: the eight-sibling field shape end to end, two projects collapsing independently, rows staying gone across a daemon restart, work past the dispatch boundary still sparing, a result outliving its row, the refusal naming its deliveries with cancel-then-forget working, the dispatcher-rule parity pin, the unreadable-record freeze and its recovery, and the disk-before-memory write ordering. Five of them run on the CI matrix in a file that had never executed there -- the panel half of this feature had no CI coverage at all, which is how the reported failure reached a user.
v6.0.217¶
- Forget Offline Nodes clears the blocks in the same frame: the sequence-parameter blocks now leave the readout synchronously with the confirmation click -- the confirmed rows are filtered out of the cached node set and re-projected through the one path that draws the panel, with no daemon round trip, drain, or tick in the visual path at all. The daemon apply runs behind it purely as reconciliation (a row it refuses to forget, e.g. one with unresolved jobs, honestly reappears on the next directory fetch). Live-measured: the block was gone within the same frame as the confirmation (frame-number-identical before and after), with the daemon's "forgot" landing 14 frames later, invisibly. This replaces v6.0.215/216's redraw acceleration, which still routed the visual update through the background loop.
v6.0.216¶
- Forget Offline Nodes redraws immediately -- actually, this time: v6.0.215's fix marked the register due and shortened the tick cadence, but the reconcile loop captures its delay when it schedules -- so the already-armed tick still fired up to a heartbeat later and the forgotten rows kept sitting in the list (field-caught within the hour). A confirmed forget now supersedes the armed tick outright (the same generation-bump rule a save's reinit storm uses) and arms a fresh one two frames out. Live-measured on a real offline row: pulse to redrawn list in under seven seconds end to end, ~1-3 s after the confirmation click.
v6.0.215¶
- Forget Offline Nodes redraws the list immediately: the forgetting always happened at once, but the node list only redrew on the next register heartbeat -- up to a minute later -- so rows the user had just confirmed away kept sitting there and the button read as broken. A confirmed forget now marks the register due and drops the tick to its minimum, so the list rewrites within a few seconds; a cancelled dialog leaves the schedule untouched. Two new tests pin both.
v6.0.214¶
- Manager container alignment fix: the manager UI's container on the Embody COMP now justifies its children to the top (
justifyv: top), correcting a minor layout issue in the panel.
v6.0.213¶
The duplicate-node endgame: the Convoy App updates itself, says what code it runs, and the last two ghost classes are gone.
- The Convoy App now updates itself: the daemon is where all node cleanup runs, but it only ever updated through a manual Repair pulse -- so releases shipped while every deployed daemon silently kept running old code, and each registry fix "didn't work" in the field. Now, when a TouchDesigner project registers with a daemon running older code than the Embody in front of it, that Embody updates the daemon in place automatically (once per session, a few seconds after registration settles). Strictly-older only: an equal, newer, or dev-tree daemon is never touched, and downgrades remain refused. Perform Mode is never interrupted (the check waits, silently, until the show ends), an externally-supervised daemon is never overwritten (the log says to update it through that supervisor instead), and an up-to-date daemon costs one check per session, not one per heartbeat. Repair Convoy App stays as the manual path.
- The daemon reports the code it actually runs:
/statusand the registration response now carry the daemon'sapp_version, read from the versioned install directory the running code lives in -- never frominstalled.json, which lies in exactly the failure this exists to catch (an update that wrote files but never restarted the process). A pre-6.0.213 daemon sends nothing, which is itself the update signal. The unauthenticated/healthroute deliberately does not carry it: a version string is a fingerprint, and it stays behind the IPC token. - Install verifies the daemon it restarted: after an install or repair, the restarted daemon is asked what version it runs; a mismatch surfaces as a visible warning ("the payload it runs may be stale") instead of a clean "installed" over stale code, and an inconclusive check says "version unverified" rather than reading as confirmed. This caught a real incident the same day it was written.
- Save-As ghosts retired (duplicate root cause #5): saving a project into a different folder re-registers the same live TD under a new project root, and the old row kept a port nobody could ever clear -- immune to the supersede sweep (root mismatch), the eviction sweep (port guard), and even Forget Offline Nodes (port-bearing refusal). The sweep now recognizes a cross-project row carrying the same runtime (minted once per TD launch, survives every save) and the same COMP as this registration's own past self and retires it. Two Convoy COMPs in one project -- which legitimately share a runtime and port -- are explicitly spared.
- Reclaimed ports are cleared: a loopback port is exclusive per machine, so an old row claiming the very port a new registration just proved it owns can no longer answer as that node. The row survives (a crashed project's node is still real and remotely launchable) but its stale port claim is cleared, which un-blinds the eviction sweep and Forget Offline Nodes.
- Forget Offline Nodes names other computers: pressing it while the only offline rows belong to another machine used to answer "no offline nodes to forget" -- accurate, and it read as broken. The all-clear now names the computers that own those rows and says to run Forget Offline Nodes there (each machine can only forget its own).
- Twenty-two new tests: nine in the daemon suite (Save-As retirement with and without a port, the sibling-COMP spare and its either-order convergence pin -- an adversarial review caught that clearing a sibling's port mid-transition would also wipe the runtime evidence its own retirement needed, leaving an order-dependent permanent ghost -- port-reclaim clearing, metadata-never-authority, the unresolved-work spare, version reporting, the version parser) and thirteen TD-side (the update decision table, once-per-session, busy-slot deferral give-back, Perform-Mode silence, external-supervisor respect, the register wiring and stale-instance pins, the client version pass-through, install verification warning and quiet paths, the remote-hosts all-clear).
v6.0.212¶
Dialogs you can actually read.
- Every dialog wraps its prose to roughly 10-15 words per line (~70 characters):
ui.messageBoxsizes itself to its longest line, so unwrapped sentences made screen-wide dialogs. One choke point does it -- authored structure (paragraphs, bulleted lists) is preserved, list items wrap with a hanging indent, and long tokens like file paths are never broken. Every directui.messageBoxcall now routes through the central_messageBox, which also makes all of them seedable and freeze-proof under the test runner. - Forget Offline Nodes answers visibly when there is nothing to do: pressing it with no offline rows now shows a small "No offline nodes to forget" message box instead of only a log line that made the button look broken exactly when it had worked.
- Six new tests: five pinning the wrapping contract (width, structure preservation, hanging indents, unbroken tokens, the choke-point wiring) and the visible all-clear.
v6.0.211¶
Convoy: one project is one row -- and you can clear the rest yourself.
- Live-save ghost duplicates fixed (duplicate report #3): TouchDesigner's versioned save renames the
.toewithout the process exiting, so the same TD re-registered under a new identity while its old row kept an Envoy port that now belonged to its successor -- the supersede sweep read that row as "live" and spared it forever. The sweep now recognizes an old row carrying the same port or process as the new registration for what it is -- the same node wearing its previous name -- and retires it on the spot. A genuinely different live server on the same project (a second TD holding an older version file open) is still never touched, and unresolved work still spares any row. - New "Forget Offline Nodes..." button on the Convoy page, right under the nodes list: names the offline rows it would remove (the first eight, then a count) in a confirmation dialog (with its consequence: a forgotten node rejoins as a new identity and TD Python approval resets), keeps nodes with unresolved jobs, and skips anything that came back online before you confirmed. This is the human-judgment path for offline rows whose project files still exist; everything mechanical already clears automatically.
- Eleven new tests: four in the daemon suite (versioned-save retirement, its port-only and pid-only variants, the different-live-server spare), six consent-flow tests (naming with cancel-touches-nothing, per-row forget, back-online skip, unresolved-work keep, no-rows-no-dialog, busy-slot ignore), and a parameter-contract pin on the new pulse.
v6.0.210¶
Convoy gets its global OP shortcut.
op.Convoy: the convoy COMP now carries the global OP shortcutConvoy, so scripted access isop.Convoy.ext.ConvoyExt.listNodes(...)instead of the path chainop.Embody.op('convoy').ext...-- shorter, rename-proof, and symmetric withop.Embody. Docs examples and the wizard's node-name fill use it; Embody-internal code keeps its parent-relative references per the referencing rules. A contract test pins the shortcut in the exported network.
v6.0.209¶
Convoy: Repair works while the app is running, and the buttons say what they repair.
-
"Host App" buttons renamed to "Convoy App": Repair/Start/Stop/Uninstall Convoy App -- "host" was unexplained jargon; the label now says which app it manages. Parameter names are unchanged (only labels, help, status strings, and docs), so nothing scripted against the parameters breaks.
-
"Bootstrap failed: 5: Input/output error" fixed: Repair Convoy App re-runs a full install by design, but the macOS register step bootstrapped straight onto the still-loaded LaunchAgent label -- which launchctl refuses with EIO -- so repairing a healthy, running daemon always failed (repairing a stopped or crashed one worked, which is why this survived until now; the daemon itself was left untouched and kept running the old code behind a false "Install failed" readout). The installer now asks the running daemon to exit gracefully and waits, then disables and boots the loaded label out and waits for launchd to actually drop it, before enabling and bootstrapping the new agent. On Windows the same graceful exit closes a silent twin:
schtasks /Create /Frewrote the task definition while the old process kept running the old code. - One warning instead of three: the "no usable interpreter" warning fired three times per host action because the venv path was resolved once per context field; it is resolved once per context now. It also no longer fires at all on a never-saved project, where the derived
.venvpath points into TouchDesigner's default folder and is meaningless by construction. - Seven new installer tests: repair over a loaded label (the exact field sequence), a label that lingers after bootout (the async-teardown settle), graceful-exit ordering on both platforms, a genuinely failing bootstrap still surfacing verbatim, and the bounded label-settle wait on injected time -- plus an in-TD pin on the ConvoyExt-to-installer graceful-observer wiring.
v6.0.208¶
Convoy: stale node rows clear themselves.
- Automatic stale-node eviction: abandoned projects lingered in the Convoy Nodes list as Offline rows forever -- only a clean unregister ever removed anything. The host app's retention sweep now forgets a node whose
.toehas been deleted from disk (after ~30 minutes of silence) and any node unseen for 30 days, while never touching a node that is merely offline (a closed TD stays listed and remotely launchable, as documented) or has unresolved jobs. An unplugged or unmounted drive is never treated as a deletion -- though the 30-day horizon still applies to it like any other silent node. Evictions are audited, and the otherwise-orphaned launch profile is cleaned up too -- a gap the manual forget path also had. - New
convoy_forget_nodetool: the daemon's/nodes/forgetrecovery route existed fully tested but had no caller; it is now exposed as a bridge tool for immediate manual cleanup of a specific stale row (refuses while the node still has work; local host only). - Eleven new daemon tests cover the sweep (dead project evicts, existing project survives, grace windows incl. daemon boot, unresolved work, live node, retention horizon, unplugged-volume protection, the reap-cadence chaining, post-restart eviction from the durable stamp, profile deletion). Filesystem probes run outside the app lock, so a hung network volume can stall only the sweep, never the host's routes.
v6.0.207¶
A fresh install stops scattering files into TouchDesigner's default folder before the project is saved.
- No more orphaned files from a never-saved project: dropping the tox into a brand-new project immediately created a
logs/folder,.embody/(with the op-type catalog), andproject1/externalizations.tsv-- all rooted in TouchDesigner's default location, orphaned the moment the wizard's save step gave the project its real home. Every writer Embody fires on its own initiative now waits for a saved project: file logging pauses (ring buffer and textport unaffected) and resumes with the first post-save log line; the catalog scan still runs but holds its result in memory and lands on disk via a post-save flush; the externalizations table defers and is swept in by the first save;.embody/local.json/project.jsonwait for the post-save hook that already writes them. (Settings persistence to.embody/config.jsonstays as-is by design -- it only fires when a persisted setting actually changes.) MCP'sexternalize_opreportsdeferred: truewhen it tags on a not-yet-saved project. Two contract tests pin every gate (they fail against the previous build).
Also in this release -- Convoy nodes stop registering under TouchDesigner's throwaway project name.
hostname / NewProject.1node names fixed: the Node Name parameter is filled once per machine at extension load -- which on a fresh install ran before the wizard's save step, baking the unsaved project's placeholder name in as if the user had typed it. A node saved ase3during the wizard then registered asNewProject.1forever. The fill now waits until the project is saved (until then the automatichostname / toe-stemis computed live from the real.toeat each registration; the wizard's save step fills it the moment the save lands), and an already-baked placeholder for this machine is healed in place on load -- genuine user-typed names never match the placeholder pattern and are untouched. The corrected name re-registers automatically on the next heartbeat.- Five new contract tests pin the fill's ordering (waits for save, stamps the saved stem, heals the baked placeholder, never clobbers a real override, leaves expression-mode alone); the heal and wait-for-save tests fail against the previous build.
- Housekeeping: eighteen vestigial Callbacks-DAT references on toolbar and window-header widgets (dangling names from an old rename, plus inert stock templates) cleared -- they warned on every reload and carried no behavior.
v6.0.206¶
Wizard: the save step's button actually works.
- Dead "Save the project now" button fixed: all wizard clicks route through one panel-execute DAT whose panel pattern lists each option group explicitly -- and the new save step's group was never added to it, so on a fresh (never-saved) project the save card rendered but ignored every click, wedging the wizard with Next locked (field-reported on macOS). The group is now wired, verified end-to-end with synthesized real mouse clicks through the panel pipeline -- the earlier screens-only verification called the click handler directly and could not see this.
- One dialog per click: the router fires selection cards on both the press and release edges (idempotent for selections); the save card is an action, so it now fires on the press edge only -- a canceled save dialog no longer immediately reopens.
- Routing regression test: a contract test now pins every option group to a matching entry in the click router's panel pattern (it fails against the v6.0.205 file), plus a source pin on the save card's single-fire exclusion.
v6.0.205¶
Wizard: the Back/Next footer can never leave the panel again.
- Footer cutoff fixed: v6.0.204's self-sizing descriptions grew three steps (Enable Convoy, externalize, footprint review) past the wizard's fixed 440px height, clipping the Back/Next buttons off the bottom. The panel is now 520px -- sized to the tallest possible step -- and the previously dormant fill spacer between the options and the footer is active, so the footer sits pinned at the same bottom position on every step instead of floating up under short content.
- Layout regression test: a new contract test recomputes every step's worst-case stack (chrome + gaps + auto-sized hint + option group) straight from
wizard.tdngeometry and pins it under the panel height, so a future text edit that outgrows the panel fails in CI instead of clipping in the field. - Also fixed: a dangling
title_callbacksreference on the window header's title widget; the wizard geometry and logic files now trigger the CI test run that guards them; the setup-wizard docs document the save step, refresh both screenshots at the new layout, and correct the Convoy save language (the wizard path now guarantees a saved project --Waiting for project saveremains the parameter-page path's status).
v6.0.204¶
The Setup Wizard gets a save gate and a consistent layout.
- Save your project first -- as step 1: everything the wizard sets up (.venv, AI config, .embody state, the optional git repo) lands relative to the project folder, so an unsaved project scattered it into TouchDesigner's default location. A never-saved project now gets a save step before all others: its card opens the OS save dialog (the ctrl-s equivalent), saves the project where you choose, re-probes the folder-dependent steps (git presence, externalization need) against the real location, and keeps Next locked until the project is actually on disk. The Convoy step's "SAVE YOUR PROJECT FIRST" warning and the recap's special case are gone -- unreachable behind the gate (the parameter-page enable keeps its own
Waiting for project savehandling). - Wizard descriptions size themselves: the hint area had a hardcoded two-value height that clipped long descriptions (Convoy) and pushed some pages' option lists lower than others (permissions). It now sizes to its text, so every page's options start the same gap below the description. Every wizard screen was capture-verified after the change.
- Also fixed: a stale callbacks reference on the toolbar's status widget surfaced by the day's extension reloads.
v6.0.203¶
HTTPS from TouchDesigner finally verifies on macOS.
- Self-update works on macOS (and every other HTTPS call from TD's bundled Python): the updater's check and download,
get_docs' derivative.ca fallback, and the PyPI version check all used bareurlopen. Windows worked only because CPython reads the OS certificate store there; macOS's bundled Python has no default CA path, so every one of these failed withCERTIFICATE_VERIFY_FAILED-- the perennial "Update check failed" on Macs. All four sites now use a verifying context that loads certifi's CA bundle (certifi ships inside TouchDesigner) in addition to any system defaults. Verification is never disabled or downgraded -- the same context feeds the self-updater, where an unverified download would be a supply-chain hole.
v6.0.202¶
The Mac field test's second find: a first-install session could wedge itself.
- A dead drain chain can no longer wedge Convoy: the worker side of a host install did everything right, but the frame-side poll chains that deliver worker results had two silent death modes (an unhandled exception in a drain, and the stale-instance early-return), and both exited without clearing their slot's busy flag -- host status froze stale and every safety-policy toggle answered "still in progress" over a call that had finished within seconds, until a TouchDesigner restart. All three slots (registration, host, policy) now recover instead of refusing: a crashing drain logs its full traceback and clears the slot, the busy guards deliver a parked result on the spot, and a flag past its 15-minute wall-clock bound is cleared loudly (wall-clock deliberately: frame-based caps stretch arbitrarily on a throttled or backgrounded TouchDesigner). Nine new contract tests pin every recovery branch.
v6.0.201¶
Convoy enables on macOS. The blocker was never architecture: TouchDesigner's bundled Python is code-signed with macOS library validation, so launched standalone it refuses every PyPI native module -- and the one line of the traceback that said so was being truncated away.
- Convoy builds its own daemon Python on macOS: when the probe finds TouchDesigner's Python signature-blocked (
runtime_crypto_signature_blocked, matching both macOS refusal variants), Convoy builds a dedicated venv at<data>/runtime-venvfrom a Python outside TouchDesigner's signature domain -- Homebrew'spython3, probed by absolute path because a GUI TouchDesigner's launchd PATH hides it -- installs the pinned cryptography, and proves it with the same Ed25519/X.509/TLS 1.3 probe every candidate faces. A healthy daemon venv from a previous enable is probed first and reused (no rebuild, works offline); the uninstall preview names it as retained; Apple's/usr/bin/python3is version-gated out (still 3.9.6) and its Command-Line-Tools shim is never spawned when the CLT are absent (that pops Apple's install dialog from a background worker). - Probe failures are classified and the diagnosis survives:
runtime_missing_cryptography(no package -- normal for a bare python3) vsruntime_crypto_broken(wrong-architecture wheel -- repairable) vs signature-blocked, each with its own honest guidance. The one-shot venv repair (uv pip install --reinstallof the crypto pin) runs only for reasons it can plausibly fix. Probe stderr is now tail-preserving (the old head-only[:500]slice kept the stack frames and cut theImportError: dlopen ...line -- twice), the install WARNING names every rejected interpreter with a marker-centered snippet, and the full stderr rides the ring buffer at DEBUG. - Architecture flips can no longer rot a venv silently: cryptography 49.0.0 dropped x86_64 macOS wheels (48.x and older were universal2), so
embody-env.jsonnow stamps the interpreter architecture and a TouchDesigner Intel<->Apple-Silicon build swap rebuilds the environment on the next Envoy enable; x86_64-macOS interpreters cap the pin atcryptography>=3.4,<49to resolve the universal2 wheel they can load. - CI is green on both legs: the macOS 30-minute hang was a test
sendalling 16 KiB into an unread socketpair (macOS's 8 KiB AF_UNIX buffer blocks forever -- now fed from a thread); the hostops refusal was the LFS gate requiringgit-lfsbesidegit(standalone installs live elsewhere -- relaxed to sanitized-PATH + outside-the-repo, now unit-tested); the bridge's spurious re-arm was a genuine float bug (deadline = clock() + timeoutrounds UP at some monotonic magnitudes; the one-sided skip window read the 6e-14 sliver as "needs re-arm" only on fresh-boot machines -- fixed in both bridge copies with a regression test pinned at the reproducing magnitude); three lifecycle tests raced a 100 ms threaded-confirm window (raised to a 5 s ceiling they never sit out). - Install consent tells the whole truth: the A-6 dialog now says the starting interpreter may be repaired or replaced with the dedicated Convoy venv (and that this may download the pinned cryptography package), instead of naming one interpreter as if it were final. 3,329 tests (118 suites), plus the pytest matrix at 2,758.
v6.0.171¶
Convoy relays work into TouchDesigner -- autonomously, honestly, and now from TD's own side.
- The dispatcher drains itself: jobs no longer wait for a manual
/dispatch.drain_once()sweeps the queue and an opt-in background loop (--drain-interval) runs it on a timer. Getting there forced the concurrency work:dispatch_jobis now three-phase and self-locking -- read/gate/CLAIM under the app lock, the forward (up to 30 s of I/O) OUTSIDE it, the resolution back under it -- so a relay can never freeze every other route. A new host-originabledispatchingstate is the claim: a compare-and-set makes two dispatchers safe (exactly one wins a job), a refused connection RELEASES it back toqueuedto retry, and a claim left behind by a dead host sweeps toindeterminateon load -- never back toqueued, where a mutation could double-run. Four adversarial review rounds found 23 probe-reproduced defects, each round breaking the previous round's fixes: attempt-scoped in-flight tokens (a stale attempt's cleanup was erasing a live one's marker and burning undelivered jobs toindeterminate), audits that can never alter dispatch state, a state-guarded downgrade path (a failed audit after a durable verdict was destroying that verdict), parked releases that retry a failed requeue write, and unreadable-vs-absent discrimination on every resolve path. - Long operations relay end to end (
run_tests,save_project): dispatch starts the node job, the host mirrorsrunningwith the node's ownjob_<8hex>handle, and the drain thread pollsget_job_statuseach tick -- poll first, dispatch second -- until the node's verdict lands, result body copied in before the node's 24-hour window closes. The host INJECTSbackground=True,override=False, and the delivery's idempotency key, because a caller-suppliedbackground=Falsewould burn the forward to a fakeindeterminateandoverride=Truewould bypass the node's own multi-session gate. A-15 honesty extends to polling with a deliberate asymmetry: an unreachable or unanswered POLL leaves the jobrunning-- a poll is a READ, and the node's record is durable -- while a node that genuinely forgot the job terminalises only after three observations and a 60-second grace. - The bridge streams:
forward_to_httpno longer buffers the whole SSE response. Server-pushed frames reach the client the moment they arrive, notifications (includingtools/list_changed) are routed live, and the rework exposed a second, unnamed defect -- the old reader returned the FIRST progress notification AS the tool-call answer. Timeouts split honestly: the 300 s cap is absolute from the first body byte, the 60 s idle window arms only once a second frame establishes a cadence (one early notification used to shrink a long operation's ceiling and report a completed op as a lost connection), and the header phase is documented as per-recv-bounded rather than silently claimed otherwise. Every no-answer shape now answers the client -- empty body, whitespace, garbage JSON, keepalive-then-EOF, headers-then-FIN, truncated Content-Length -- two of which (a 200 with an HTML error page, a 200 with an empty body) were silent client hangs in every shipped version.EMBODY_BRIDGE_NO_STREAM=1falls back to read-to-EOF through the same parser, documented in thetd-recoveryskill. - TouchDesigner registers itself (Convoy Phase 2): a new
Convoyparameter page --Convoyenable,Convoyid,Convoystatus-- and aConvoyExtchild COMP that reconciles a desired-state tuple on a generation-guarded tick. An unchanged tuple issues ZERO network calls; a changed one kicks one bounded worker through the house main-thread/worker/poll pattern.runtime_idis minted once per TD process (surviving extension reinit, not a restart), the 30-second heartbeat re-supplies the per-launch Envoy port a host restart forgets, and absence is silent: no host app means the status readsNo Convoy host app, one debug line, a slower tick -- never an error, never a dialog. The first enable asks, naming the convoy id it will mint, stating that the id and the consent land in a committedproject.json, and recording the granted scope (local host app only) so a future LAN phase must ask again. Cancelling writes nothing. - Two defects found by drafting against the real code: the Embody execute DAT's
exitcallback had never been armed, soexecute.py'sonExit()has been dead code -- every shutdown hook it was meant to run has never fired; and_convoy_genneeded aSKIP_STORAGE_KEYSentry orconvoy.tdnwould churn on every save. Both fixed and pinned. - Two defects the release gates caught, both older than this work: the
_TRANSIENT_STATUS_PARSinvariant refuses a''resting (an empty string cannot be a state the enable machine leaves) -- correct, and it caughtConvoyidregistered that way. An identifier that legitimately rests empty wants the registry's other mechanism,None, which the scrub reads as "reset to the par's own default"; the invariant now names that distinction rather than being widened. Separately,raise SkipTestinsetUp-- the documented way to skip a whole suite -- was caught by the test runner's broadexcept Exceptionand reported as an ERROR, in BOTH runner paths. Every class-level skip Embody has ever written was miscounted; the new integration class simply did it fourteen times at once. Fixed, and the fourteen are now honest skips. - Convoy's pure-Python suite grew 356 -> 462 (12 files), the bridge suite 254 -> 339, and a new in-TD
test_convoy_extadds 40. 2,895 tests (114 suites). Verified live, not just green: a job submitted with no hand-supplied port was dispatched by the autonomous loop into a self-registered TouchDesigner and returned real network data;run_testsrelayed through the fullqueued -> dispatching -> running -> succeededlifecycle with node provenance; disabling Convoy cleared the port host-side; and a save confirmed this machine's convoy id reaches zero tracked files.
v6.0.169¶
Perform Mode no longer fights the Envoy liveness watchdog.
- Perform Mode is now a watchdog idle condition: entering Perform Mode calls
Stop()on the Envoy server but deliberately leavesEnvoyenableon (that parameter also drives config deployment), so the liveness watchdog read the resulting enabled-but-down state as an outage -- it probed the dead socket, revived the server ~4-12 seconds into every performance, and overwrote thePerform Modestatus readout withReviving (watchdog)...while the Envoy parameters sat greyed out and contradicting it. The thread-exit restart hooks already guarded on the Perform signal; the watchdog path never got the guard (the asymmetry that marked this an oversight, not a design). The tick now gates on the same authority the hooks use -- a new_performModeActive()helper reading the livePerformmodepar, never the status string, whichStop()and the hooks overwrite -- resets its dead-tick counters while performing, and resumes normal revive duty the moment Perform exits. Start()refuses while performing: a revive or auto-restart queued before Perform entry could still land mid-show and bring the server back beside the watchdog's own gate.Start()now refuses with a clear WARNING while Perform Mode is active. The Perform-exit restart is unaffected:_exitPerformModeruns after the live par is already off, so its delayedStart()passes the gate.- In-flight starts can no longer cross a Perform entry (adversarial-panel finding on the fix itself): a start already inside its startup window when Perform Mode was entered used to complete mid-show --
Stop()no-ops whileenvoy_runningis still False (the entire window), and the three startup polls (_pollImportGate,_pollBootstrap,_pollStartup) finished the start or declaredRunning on port Nover the Perform readout, bypassing theStart()gate entirely. Realistic trigger: enter Perform seconds after a cold open while Envoy is still preparing its Python environment. All three polls now honor the Perform authority -- the two pre-start polls abort, and a worker that bound mid-entry is torn down instead of declared. The thread-exit hooks were also unified onto the same helper, so a broken extension reference now degrades to a scheduled restart instead of raising out of the hook and scheduling nothing. - TDN files export with LF line endings: the TDN writer's
os.fdopendefaulted to newline translation -- CRLF on Windows -- so every.tdnre-export flipped a committed file against the.gitattributes*.tdn eol=lfdeclaration, the same churn class v6.0.168 closed for generated rules and skills. The writer now pinsnewline='\n'; reads use universal newlines, so existing CRLF files still parse with no migration. - Twelve new watchdog tests across three classes pin the contract both directions.
TestWatchdogPerformMode: Perform on -> no probe, no dead-tick accrual, no revive, status readout untouched across repeated ticks (asserted against the REAL revive body, not a stub); Perform off -> the shipped enabled-but-down revive (the save-wedge incident fix) keeps firing;Start()refused mid-Perform but passing after exit; the watchdog resuming full revive duty post-Perform.TestPerformModeAuthority: the real helper wire exercised unstubbed -- it matchesEmbodyExt._performMode, and an exception in the chain reads False so a broken reference can never disable self-healing.TestPerformModePollGates: both pre-start polls abort during Perform, the just-bound-worker teardown, and the Perform-off declare-Running passthrough. The Perform signal is otherwise stubbed at the extension instance -- toggling the live par mid-suite would sever MCP with no command path back. 2,516 tests (112 suites).
v6.0.168¶
Embody's own generated files stop showing up as phantom git changes.
- Generated rules and skills no longer re-dirty themselves on every deploy:
Path.write_text()defaults tonewline=None, which translates every line feed toos.linesep-- CRLF on Windows.write_template(the deployer behind.claude/rules/*.mdand.claude/skills/*/SKILL.md) used that default, so each deploy rewrote every generated file with CRLF while.gitattributesdeclares*.md eol=lf. The result was a permanent row ofMbadges whosegit diffis EMPTY -- and in a user project without a.gitattributes, CRLF committed outright and then churned against collaborators on macOS and Linux. Ten write sites now pinnewline='\n':write_template, the CLAUDE.md / ENVOY.md writers, both JSON manifests, and the.gitignore/.gitattributeseditors. The nine files already carrying CRLF were normalized once so the noise clears immediately. .embody/project.jsontoo: it is the one file under.embody/that.gitignoredeliberately un-ignores (lines 35 and 75), so it is committed in every user project -- and its atomic writer had the samewrite_textdefault. It only rewrites when the TD build changes, which is why the churn is rare enough to have gone unnoticed. Its writer and the sibling settings writer now pin LF as well.- Diagnosis note for the next reader: a CRLF-vs-LF mismatch reports asymmetrically --
git statuslists the file as modified whilegit diffshows nothing, because the eol attribute normalizes on compare but the working blob still differs from the index.git diffon the path (orgit update-index --refresh) re-hashes and clears it when the content really is identical, which is why the badges sometimes vanish on their own and look intermittent. Repo-wide, 103 tracked files carry CRLF from ordinary Windows editing; those are inert because nothing rewrites them. Only the files Embody itself regenerates churned, which is why the fix is scoped to Embody's writers rather than a repo-wide normalization.
v6.0.166¶
Multi-agent flow: a shared task ledger so parallel sessions know work-STATE (not just presence), and long operations become restart-proof background jobs.
- The shared task ledger (
announce_task/update_task,.embody/tasks.json): claims answer "who is touching what right now" -- nothing recorded what state the WORK is in, and it cost real time the same morning it was built: a session read another session's FINISHED-but-uncommitted feature as in-flight and held its own batch behind it. Sessions now announce substantive work with the scopes it touches, flip it todone_uncommittedthe moment it is complete-but-uncommitted (the load-bearing state -- it never expires on its own), and record the sha on commit (commit=alone implies the transition). Every session sees active entries onget_sessions;preflight_landingreports ledger tasks overlapping a landing and warns loudly when one isdone_uncommitted. A day-silentin_progressis flagged stale; a fortnight-silent one auto-abandons (attributed to_ledger_prune) so the file cannot rot unbounded. Cross-process merging is per-id newest-updated-wins -- the pre-landing review caught a blanket disk-wins that rolled a process's own freshdone_uncommittedback toin_progress, precisely the misread the ledger exists to prevent. The convention ships in themulti-sessionrule and etiquette skill (and their templates), so every configured project's agents participate by default. - Background jobs for long operations (
get_job_status;run_tests background=True; newsave_project): a full test run and aproject.save()both outlive the 30-second operation timeout, and a mid-operation server restart severed the synchronous call even though the work completed -- observed twice in one day ("Server force-restarted during test run"; a save returning IncompleteRead). Job-mode tools return ajob_...handle immediately and park results in.embody/jobs/, which survives server restarts and extension reinits;get_job_statuspolls, and a finished record carries the run summary (counts + failing tests) orversion_before/version_afterfor a save. A running record that stops updating is flagged stale rather than trusted. Verified over the wire: the handle returned in 0.46s and a full suite completed into the record while the watchdog suites restarted the server under it. The registry is the deliberate substrate for MCP's Tasks extension when clients adopt protocol 2026-07-28 (see the roadmap). - The write-effect footer stops crying wolf: its first field day showed it counting TouchDesigner's OWN
/uidialog cook-loop warnings as damage from the caller's write, and surfacing the continuation lines of multi-line TD warning strings ("Parameter: From Range") as if they were operator paths. The diff now excludes TD-internal subtrees (/ui,/sys) and non-path entries up front. - The fresh-install smoke runs headless: the Setup Wizard is a panel window, not a
ui.messageBox, so seeded responses could not answer it and the smoke stalled until a human clicked (observed on the v6.0.162 gate). The harness now closes the wizard window and drives the SAME backendfinish()calls (_applyWizardSetup) with Auto-mode smoke choices -- Envoy enabled, git and externalization skipped -- so the release gate needs no hands. The settle-check race (a flag written during the catalog scan, before the opt-in ever fired) was fixed alongside. - Tool inventory honesty: counts move 56 -> 60 (
announce_task,update_task,get_job_status,save_project) across every enforced claim site, and the agent-tier inventory backfillsget_focus/get_guidance, which v6.0.165 added without listing (the agent tier does not run in a normal suite, so nothing failed -- drift caught at review). The/run-testsskill now teachesbackground=True+get_job_statusinstead of the GetResults polling dance. 2,542 tests (112 suites).
v6.0.165¶
Five agent-experience features drawn from a full code-level teardown of a competing TouchDesigner AI plugin -- each one closing a gap that teardown exposed, without adopting its telemetry, system-file patching, or runtime prompt manipulation.
get_guidance-- this project's doctrine now reaches EVERY MCP client: Embody's rules and skills (.claude/rules/*.md,.claude/skills/*/SKILL.md) only ever loaded in Claude Code. Agents on Codex, Cursor, or opencode got the tool schemas and none of the TouchDesigner discipline -- no layout invariants, no threading rules, no load-before-acting gates. A new worker-side tool (no TD round-trip) serves all 36 documents over MCP: a bare call lists topics with descriptions,topic=returns one document, and matching ignores case and punctuation socreate_operatorfindscreate-operator. It complementsget_docsrather than duplicating it --get_docsis official Derivative documentation,get_guidanceis how THIS project wants the work done.get_focus-- "fix this operator" now resolves: reports the pane's current network, the selected operator(s), the current op, and the rollover, plus atarget/targetSourcepair and a note stating the disambiguation rule outright: "this operator" means the SELECTED/current op, NEVER the rollover, which is incidental mouse position. Headless/Engine TD (no panes) returnsheadless: truewith nulls instead of raising. Deliberately no screen or desktop capture.- Write operations now report what they just broke: a compact
_effectsblock rides back on mutating tool responses carrying operator errors and warnings that did NOT exist before the call, plus a meaningful frame-rate drop. Both are diffed against a per-session baseline, so pre-existing damage stays silent and the first write of a session only establishes the baseline. The error scan self-disables for the session if it ever exceeds its time budget, so it can never tax every write on a large project, and the fps sample reads Embody's Perform CHOP ONLY if it already exists -- an unrelated write must never create a monitor operator as a side effect. - The setup wizard now offers externalization: whole-project externalization and auto-externalize-new-ops both already existed, but nothing surfaced them, so most projects stayed invisible to git and to AI tools. A new wizard step offers "externalize everything now", "new work only" (the safe pre-selected default), or "not now", and the step hides itself when the project already externalizes. The whole-project path is the one wizard action that rewrites an entire project, so it is gated: it REFUSES without a saved
.toeon disk as a recovery point (checking the file directly, neverproject.modified/project.dirty-- both have failed here in opposite directions), and it routes through the existingExternalizeProject()so the user still gets its confirmation dialog and TOX/TDN choice rather than a new silent bulk path. - Launching an AI client seeds its first prompt: the first time Embody opens a CLI agent for a project it passes an opening prompt positionally so the session starts interactive and immediately useful -- never via a
-p/--printflag, which would answer once and exit. Gated to once per project by a marker file so it is not re-injected on every launch, and GUI editors (Cursor, Windsurf, VS Code) are untouched because they have no prompt channel. Both script builders stay pure and unit-tested: unseeded output is byte-identical to before, the macOS-ilcfallback nests its quoting correctly, and a prompt containing quotes or%cannot corrupt the Windows.bat. - The new wizard step matches every other step's frame: it was first built with a tall 60px hint and a 235px option group -- 426 of the 440px content area, which pushed the Back/Next footer off the bottom edge. Shrinking the group fixed the clipping but floated the footer ~45px higher than neighbouring steps (the wizard's fill-spacer is not displayed, so the footer sits wherever the content ends). The step now uses the standard one-line 16px hint and the same 235px group as mode/assistant/client/git, so hint, first option, divider and footer all land on the same baselines as every other step -- 382 of 440, 58px slack. Verified by capturing the rendered panel and comparing it against the git step frame, not by arithmetic.
- Doc-count drift caught by its own guard: adding two tools tripped
test_version_sync, which asserts every user-facing tool-count claim against the registered count -- including a README badge a plain grep for the feature table missed. All six present-tense claims now read 56 (release-history bullets are exempt by design), and both new tools are documented in the tools reference and the shippedmcp-tools-referenceskill + its template. - Tests: three new suites --
test_envoy_agent_ux(guidance matching/scanning, the effects diff + fps-regression math, live get_focus),test_wizard_externalize(step gating, the whole-project safety rail, recovery-point refusal), and 14 new cases intest_launch_aiclient(seeded-prompt quoting on both OSes, byte-identical unseeded output, the one-time marker). Two wizard-hint tests still asserted the rolled-back tall-hint iteration and were aligned to the final single-line design at commit review. Full suite on the live build: 2,469/2,478 with those two aligned (20/20 on rerun), 7 pre-existing platform skips. 2,516 tests (110 suites).
v6.0.162¶
MCP SDK 2.0: the overnight 2.0.0 release that broke every fresh install (issue #81) is now the floor -- ported, pinned in both directions, and every existing venv upgrades itself.
- Why fresh installs broke overnight (issue #81): the MCP Python SDK published 2.0.0 on 2026-07-28 and REMOVED
mcp.server.fastmcp. Embody's dependency spec wasmcp>=1.26.0with no ceiling, so every venv built after that moment resolved 2.0.0 under 1.x server code. The import gate only checkedmcp.server-- which 2.0.0 still provides -- so the gate passed and the failure surfaced downstream as a 30-minute auto-restart storm walking ports 9880->9885. Existing venvs kept working (theirs already held 1.x), which is why no dev machine saw it: only fresh installs -- and the reporter's clear-the-venv remediation -- hit it. - Envoy now runs on SDK 2.x (
MCPServer): theFastMCPconstruction is gone, transport settings (stateless_http,transport_security,host) moved ontostreamable_http_app()per the 2.0 API, andserverInfo.versionreports Envoy's own version (2.0 reports""when unversioned; 1.x substituted the SDK's). Verified against the real wheel on TD's exact Python (3.11.15): decorator tools and prompts,Imagecapture returns,Optional[Literal]schemas, DNS-rebinding rejection (421 foreign Host / 403 foreign Origin), and an old-revision (2025-06-18) client handshake -- current Claude Code clients keep working unchanged. - The dependency pin is now a RANGE:
mcp>=2.0.0,<3, the ceiling always the next major derived fromMCP_MIN_VERSION-- an upstream major can never again be adopted by the resolver instead of by a deliberate port. - Nobody rebuilds a venv by hand again: every successful install stamps the venv (
embody-env.json) with the dep spec and Python it was built for, and_environmentNeedsInstallcompares that stamp against the current spec -- so ANY release that changes a pin auto-upgrades every existing venv in the background on its next start. Verified live during this release's own landing: this repo's venv went 1.28.1 -> 2.0.0 in place and got stamped; the reporter's poisoned-2.0.0 venv shape is adopted as-is. An mcp at/above the ceiling (the issue-#81 shape) triggers the same walk-back reinstall. - A TD upgrade that bumps embedded Python REBUILDS the venv instead of installing onto the wrong ABI:
pyvenv.cfgis probed BEFORE any stamp logic, so pre-stamp venvs and combined python+deps upgrades both take the rebuild path (review caught a deps-first ordering that would have in-place-installed cp311 wheels under a future 3.12 TD and then stamped the lie); the rebuild routes throughuv venv --clear, so uv owns the removal. - Upgrading a LIVE session refuses honestly instead of crashing: packages upgraded on disk while the old stack is imported now produce "Save your work and restart TouchDesigner to finish the upgrade." The gate detects both a recorded loaded-version mismatch and a legacy 1.x stack (by its fastmcp module, with or without disk metadata present) and REFUSES rather than importing a mixed stack -- re-running pydantic model definitions over a live pydantic_core can abort() TD. The refusal clears the process-wide fast-path flag (previously set in five places and cleared in none, so the protection survived exactly one Start before the next reinit bypassed the gate into the exact mixed import it had just prevented) and parks
Envoystatuson an Error state the liveness watchdog deliberately leaves alone. Observed live during this release's landing: install, one refusal, silence -- no storm. - The import gate now tests the module the server actually imports (
mcp.server.mcpserver) -- the parent-package check is exactly what let issue #81 through -- and the install log names the spec it resolves (Installing dependencies (mcp>=2.0.0,<3, ...)), the one line that would have made the field log diagnosable on sight. The installed-version read is version-sorted rather than filesystem-order, so an interrupted uninstall's leftover dist-info can no longer wedge needs-install or feed the gate a stale version. - 2.0's silent 4 MiB request-body cap is raised to an explicit 64 MiB: 1.x never enforced a body limit; 2.0's default 413s an oversized
tools/calland resets the connection, which the bridge reads as "Lost connection to Envoy" and drops to fallback tools. Multi-MBimport_network/set_dat_contentpayloads (multi-thousand-operator networks) stay inside the explicit cap. Localhost-only and Host/Origin-validated, so the cap is sanity, not exposure. - Textport kept clean under 2.0:
MCPServer.__init__callslogging.basicConfig-- inside TD that installs a root stderr handler and drops the root level to INFO process-wide (measured: handlers 0->1, level 30->20), turning every info-level logger in the process into textport output; Envoy snapshots and undoes it around construction. The dead 1.x lowlevel-server filter ("Processing request of type ...") is removed -- 2.0 emits neither of the messages it dropped -- and the disconnect filter now also coversmcp.server.runner, the modern-envelope path's logger, so disconnect noise does not return when clients adopt protocol revision 2026-07-28. One behavior note: 2.0 runs sync tool bodies CONCURRENTLY on worker threads (1.x serialized them inline on the event loop);_execute_in_tdwas already concurrency-safe per request, and a long TD-blocked tool no longer starves pings. - The update nag no longer sets a trap: the old notice told users to "delete dev/.venv to upgrade" -- which, on unpinned <= 6.0.160, is precisely the reproduction for issue #81 (if you are still on an older version, do NOT follow that notice; update first). It now recommends only releases INSIDE the supported major (yanked releases excluded, so the nag can never point at an unresolvable pin), notes a new upstream major as a calm informational line, and describes the real mechanism: bump
MCP_MIN_VERSIONin a release and every venv upgrades itself. - Tests: the setup-environment suite is rewritten around the stamp / ceiling / rebuild / refusal matrix (+16 tests, including the issue-#81 above-ceiling regression and the fast-path-flag bypass), and the two tests that run a REAL install now SKIP with a restart instruction when a 1.x stack is still loaded (the post-landing, pre-restart window) instead of upgrading the venv mid-suite. Full suite green on the ported server (2,378/2,386 run; the single failure was the known clipboard-contention flake, 8/8 on isolated rerun; 7 pre-existing platform skips). 2,424 tests (108 suites).
v6.0.160¶
Two guards that were silently OFF: a process-liveness check that called dead processes alive, and a dialog-suppression guard that any mid-run file edit disarmed.
is_pid_alivereported exited processes as ALIVE, permanently. Both the Embody-side check and the bridge's usedOpenProcess(SYNCHRONIZE)alone -- but on Windows a process OBJECT (and therefore its PID) stays allocated while ANY handle to it remains open, soOpenProcesskeeps succeeding long after the process exits. Measured 2026-07-27: a dev TD pid absent fromGet-Process, with its Envoy port closed, still read alive. Consequences compounded:write_envoy_confignever pruned that registry row,instance_keynever reclaimed the basename, and every relaunch of the same.toeminted a freshEmbody-6.159-2,-3, ... while each bridge's pin chased a dead instance (four manualswitch_instancecalls in one session). On the bridge side it also stranded heartbeat files for exited sessions, which then lingered as phantom peers inget_sessions/_peers. A zero-timeoutWaitForSingleObjectdistinguishes the two: the object is SIGNALED exactly when the process exits. Anything else -- includingWAIT_FAILED-- counts as alive, because declining to prune what could not be verified is the safe direction. Verified live: the registry collapsed 2 rows -> 1 and the next launch took the clean base key. The check now builds a PRIVATEctypes.WinDLL('kernel32')rather than mutating the process-widectypes.windllprototype cache that TouchDesigner itself shares.- The test-run dialog guard turned itself off on any extension reinit.
_runningwas plain instance state, and every test DAT and extension source in this project issyncfile'd -- so editing one mid-run hot-reloads the DAT and REBUILDSTestRunnerExt, resetting the flag while the deferred tick chain (which IS reinit-hardened, via its generation token) kept running tests. Two guards failed at once:EmbodyExt._testRunnerActive()read False, so_messageBoxstopped suppressing and a REALui.messageBoxescaped to the user mid-run -- an "Embody -- Uninstall" confirm, which once clicked flippedEnvoyenableoff and stopped the live Envoy server -- and the "Tests already running" re-entrancy check read False, letting a second run start on top of the first. Run-active state now lives in COMP storage so it survives reinit, stamped with wall-clock time and refreshed per test, so a crashed or abandoned run expires (90s) instead of muting every dialog forever. This also fixes the two failures it was causing:test_uninstall_handler.test_suppressed_defers_like_cancelandtest_toxdrop_expr.test_dismissed_dialog_not_remembered. - Six more runtime keys were serializing into committed
.tdnfiles._watchdog_gen,_clip_watch_genand_shortcut_rec_genare self-rescheduling-loop generation counters bumped on every reinit; they carry no meaning on disk and rewrote three lines ofembody.tdnandEmbody.tdnon EVERY export (measured across one test run:_clip_watch_gen884 -> 917,_shortcut_rec_gen503 -> 517).claudius_runningis the exact counterpart of the already-excludedenvoy_running. The two worst were caught in a mid-run export:_test_saved_status, and_smoke_test_responses-- seeded auto-answers for headless dialogs, which restored into a user's project would silently answer REAL modals, the Uninstall confirm included. - The skip list is now an INVARIANT, not a hand-maintained list. Enumerating it by hand has failed repeatedly -- three separate keys leaked in a single day, each caught only by eyeballing a diff.
test_no_live_embody_storage_key_escapes_the_skip_listasserts that nothing in Embody's live storage is absent fromSKIP_STORAGE_KEYS, so the next key added to Embody storage fails loudly until someone decides deliberately whether it may reach disk. - The clipboard suites were failing on a contended OS resource, and blaming the product. The Windows clipboard is machine-wide and guarded by
OpenClipboard(): while any process holds it, every other write silently fails andui.clipboardstill reads back the old value. On a box running several TouchDesigner instances that is routine, and it surfaced as a bare0 != 1orunexpectedly Nonepointing at the watcher instead of the environment. One shared helper onEmbodyTestCase(seedClipboard/requireClipboardHolds) retries with backoff -- contention outlasts a tight loop -- and reports a loud SKIP rather than a false failure, the same conventionrequireCliuses for a missing CLI. Every seed site in both suites routes through it. The clipboard-watch suite also orphans the live 1500ms watcher tick for its duration, so a background poll can no longer consume the signature between a test's two explicit polls. - Fresh-install smoke: a false green, and a "fix" that belonged elsewhere. The ready flag reported the Envoy OPT-IN parameter as though it were server health, so it read green while the server had actually aborted; it now waits for a TERMINAL Envoy state (not a frame count) and writes an explicit
verdictplusproblems,envoy_status,updatestatus,autosavestatusandfilecleanup. An intermediate attempt to isolate the smoke instance by flippingAiprojectroottoprojectfolderwas withdrawn -- not because that parameter misbehaves, but because the smoke template lives INSIDE this repo. Changing the AI-config root migrates it and cleans the old root, and it does so precisely:remove_if_markedunlinks only files carrying theGenerated by Embodymarker, directories go throughrmdir()(which fails on non-empty, so user content is never swept), and.mcp.jsonloses only itsenvoyserver entry with any other MCP servers preserved. Running it here proved the guard rather than breaking it -- sweeping.claude/skills/it removed the 14 generated skills and left the 7 hand-written dev-only ones sitting beside them, and leftCLAUDE.mdand every.claude/rules/file alone. The only reason it mattered is that the Embody SOURCE repo is the one project where Embody-generated files are themselves committed to git (they are the source for the shipped templates); in a normal user project nothing is lost, since everything is regenerated at the new root. Isolation has to come from the working directory instead: the harness now takes the repo root fromEMBODY_SMOKE_REPOso the template can run from a temp copy, and warns loudly when it is running in place. 2,407 tests (108 suites).
v6.0.159¶
Field-report triage from a 1,700-operator project upgrading to 6.0.157: a dead MCP tool restored, a silent per-save file deletion closed, TDN sequence export made reliable on uncooked POPs, and a whole class of runtime state stopped leaking into version control.
remove_externalization_tagwas DEAD, not degraded (since v6.0.154): the registered tool wrapper putdelete_filein the params dict unconditionally and dispatch ishandler(**params), but the main-thread handler accepted onlyop_path-- so EVERY call from EVERY client returned{'error': "... unexpected keyword argument 'delete_file'"}. Two releases shipped that way with the full suite green, because no test had ever invoked a registered tool wrapper. Fixed, and verified live before/after.- New guard: MCP tool-schema conformance (
test_envoy_tool_schema, 8 tests). Static AST analysis over all 54 registered tools checks three directions of wrapper-vs-handler drift: a forwarded key the handler cannot accept (TypeError, tool dead), a required handler param never forwarded (same), and an advertised param that is neither forwarded nor consumed locally (a SILENT no-op, the worse failure). Also asserts no duplicate operation dispatch and that every params dict stays a plain literal, so the analysis can never quietly degrade. Audit result on the rest of the surface: clean. - Silent
.tdndeletion on every save:checkOpsForContinuityresolved tracked operators with a bareop(), which cannot resolve a UTILITYannotateCOMP(measured on 099.2025.33070: the utility flag hides the node from its parent's lookup; paths through it still resolve). A legacy row AT an annotation therefore read as a VANISHED operator on everyUpdate()/save, sending it to the file-cleanup modal -- or, withFilecleanup='delete', to a silentunlink()of the.tdnplus its registry row. Now consults_isAnnotateInteriorPathfirst. - TDN sequence export dropped populated sequences on uncooked POPs: discovery used
target.pars()+p.isSequence, which misses sequences entirely until the operator cooks (freshlinePOP: pars-discovery sees['attr']while iteratingtarget.seqseesattr(1)+pt(2)). Discovery now iteratestarget.seq-- the path the importer already trusts -- which also materializes the block parameters the per-block export needs. This is a load-bearing ordering contract, documented and test-guarded. - The exporter can no longer emit a file its own reader rejects: an empty block list is omitted (with a warning) instead of written as
name: [], which TD refuses to import (Minimum size is 1 block) and which madediff_tdnreport the COMP permanently changed. On import, an empty list is SKIPPED, never clamped to the minimum -- clamping would setnumBlocks=1and destroy a live multi-block sequence. - Three sequence setters misreported missing sequences:
_getSequenceByNamereturnsNonerather than raising, sotry/exceptaround it never fired and a missing sequence surfaced as "Failed to set numBlocks/blockSize". All three sites now check forNoneexplicitly. remove_externalization_tagresponse fidelity: addsremoved_rows,removed_anything, and a human-readablesummary.removed_tagsalone could not distinguish "nothing happened" from "the registry row went but there was no operator tag to strip" -- the exact shape a pre-guard annotation artifact has. Row accounting is a multiset difference by identity, not anumRowsdelta, so a concurrent session cannot perturb it.- Annotation-artifact warnings collapsed: one warning per annotation with a row count, instead of one per row per save (16 near-identical lines in the field report). The dedup set moved from the extension instance to COMP storage, so an extension reinit -- which every save triggers via the TDN strip/restore -- no longer re-arms the whole flood. The remedy now leads with the non-destructive option and no longer suggests deleting the annotation first.
- Removal primitives are utility-aware:
RemoveListerRow,RemoveTDNEntryand_removeOrphanedTDNChildrenuse a new guardedresolveOpIncludingUtility. Previously a legacy row AT a utility annotate dropped the table row but left the tag, colour and_tdn_rel_pathbreadcrumb behind, so the next Refresh sweep resurrected it.resetOpColornow exempts annotations, so the cleanup keeps its promise not to restyle them. .gitignorestopped duplicating its managed header: the writer computed only the MISSING entries but wrote a fresh# Embody / Envoy (auto-managed)header every time, so each release that added an entry appended another header block (this repo accumulated three). Entries now append inside the existing block, duplicate all-managed blocks are consolidated (a block containing user content is never merged), and the order-dependent.embody/*/!.embody/project.jsonpair is enforced on final content -- git is last-match-wins, and inverting it silently untracks the committedtd_buildpin.- Two silent config-migration bugs fixed: the
.gitignorestale-entry migration was computed and then discarded by an early return, so a documented migration had never actually run; and.gitattributesreturned on any existing marker, so attribute lines added by later releases were never backfilled into an existing install. Both writes route through the Advanced-mode consent guard, with an honest action string on the branch that DELETES lines. crash_detectedno longer sticks after an external TD relaunch: withstate.td_pidstill naming the dead process, the reconciler RE-ASSERTED the flag on every heartbeat tick, soget_td_statusreported a crash forever while TD was alive and healthy. It now clears on reconnect and re-resolves the pid -- honouring this bridge's instance PIN, so a pinned session can never adopt a foreign project's TD pid. Liveness checks inreconcileandconnection_lost_messagemoved to the recycled-pid-safeis_td_process_alive.- The TDN export progress dialog leaked its last run's state into version control:
_closeExportProgressclosed the window but never reset the status label or the progress-bar width. Those are ordinary parameters on COMPs inside Embody, so whatever the last export left there was captured by Embody's own.tdnand committed -- a test-run label (sandbox_test_tdn_export_progress -- 400 / 1,000 operators (40%)) had been sitting in the repository across several releases. Both are now reset to their parameter defaults on close, so the dialog contributes nothing to the exported document. - MCP tool count had drifted: the README badge, the README feature table, two lines in
docs/index.md, and three on the published Envoy docs pages all said 53 while 54 tools are registered (confirmed against the bridge's advertised list: 58 = 54 TD-side + 4 bridge meta-tools). Unlike the version badge,updateVersionDocsdoes NOT rewrite this number and nothing asserted it, so it drifted silently. All seven are corrected, andtest_version_syncnow checks the present-tense claims in README.md, docs/index.md, docs/envoy/index.md and docs/envoy/tools-reference.md against the registered count (release-history bullets are exempt -- they state the count as it was for that version). NOT yet covered: the marketing/machine-facing surfaces underweb/andplatform/apps/web/public/(llms.txt, for-ai.*), which still advertise older counts. - The annotation-warning dedup must not persist: an intermediate version of this release stored the warned-set in COMP storage to survive extension reinit. Storage is serialized into the
.tdn/.toeexport, so a release save wrote a test-sandbox path into the committedEmbody.tdnAND would have silenced the warning permanently for every future session. Reverted to an instance attribute -- the per-annotation aggregation already does the heavy lifting -- with a regression test asserting the key never appears in storage. - A release rename no longer degrades crash detection: every version bump renames the
.toe, so the instance name changes (Embody-6.157->Embody-6.159) and each bridge's pin goes stale on the very next open. The new pin gate then refused to adopt the instance's pid -- correct for a foreign instance, wrong for our own under a new name. It now also adopts when the resolved instance answers on the port the bridge is ALREADY using, which a genuinely foreign instance never does. Caught on the live v6.0.159 smoke run; a mutation harness confirms widening that clause to any port is detected. - The destructive test tier could not run at all, and its save-gate had failed in BOTH directions.
RunDestructiveTestsguards the suites that exerciseDisable/ExternalizeProjectagainst the live project. Its original check usedproject.dirty, which does not exist on TD 2025 --getattr(project, 'dirty', None)returned None, so the gate was silently OFF (that is how a normal run reached these suites and deleted 18 specimen.tdnfiles on 2026-07-01). The fix for that switched toproject.modified, which Embody's own post-save housekeeping (Refresh sweep, TDN re-export, table writes) re-dirties within SECONDS -- so the gate then refused even immediately afterproject.save()(measured: true in 6/6 samples ~2 min after a successful save). Net effect: the Disable/Enable lifecycle these suites exist to protect had never actually been exercised. The gate now checks the real invariant -- a saved.toeexists on disk atproject.folder / project.name-- and LOGS that recovery point with its age so the caller can judge it.confirm_saved=Trueremains the opt-in. test_disable_z04_verify_completeasserted on another test's leftovers. It read the externalizations table expecting z03's TDN externalization to survive an interveningtearDown, a base teardown, and deferred Update/delete work still in flight from three priorDisablecalls -- justified by "runs one frame after z03 so deferred Updates have settled", whichRunDestructiveTestsdoes not provide (it drives_runSuitesynchronously). It now re-externalizes for itself, so every assertion describes its own action. Its assertions also carry the table's shape (rows=N by_extension={...} by_strategy={...}) and an explicit non-empty check: the previous bare0 not greater than 0could not distinguish an EMPTY table -- where the two preceding assertions pass VACUOUSLY -- from a populated one with no.tdnrows, which is what made diagnosis slow. Destructive tier: 32/32 passing, its first green run.- New guards for the untested axes the field report exposed: writer/reader contract invariants (
test_tdn_roundtrip_invariant, 6 tests -- asserts the exporter never emits an unimportable document), config-writer behavior across a VERSION BUMP (test_config_migration, 9 tests -- the migration axis no single-run test can see), the annotate/continuity interaction (test_annotate_continuity, 7 tests), and bridge crash-flag TRANSITIONS rather than states (5 new tests, including foreign-instance mistargeting). 2,400 tests (108 suites).
v6.0.157¶
Multi-everything bridge routing: per-session instance pinning ends registry hijacks, worktree tasks become durable and visible, and landings get an automated preflight.
- Per-session instance pinning: each session's STDIO bridge now pins to a TouchDesigner instance by NAME and re-resolves its port from the registry every tick -- a pinned instance restarting on a new port still self-heals, but registry churn can no longer re-target a session. The registry's
activefield is demoted to the default seed for newly-spawned bridges.EMBODY_PIN_INSTANCEenv var pins a session from birth. Any N toes x M sessions mapping is now supported. switch_instanceis session-local: switching re-pins only the calling session's bridge; peers are untouched. The old move-every-window behavior is available explicitly via the newall_sessions=trueparameter, which writes the registry default and bumps a newactive_epochcounter -- pinned bridges treat an epoch increase (and only that) as a command overriding their pin.- Registration is adopt-if-vacant: a freshly-starting Envoy instance takes the registry default slot only when it is empty or names a dead instance. This structurally fixes the 2026-07-25 incident where a fresh instance's registration yanked every live session's bridge to it (once to a dead port). Verified live in this release's smoke: a second instance registered while five sessions worked -- zero bridges moved.
- Durable worktree claims:
project:worktree-*claims persist to.embody/worktree-claims.json, survive session death AND Envoy restarts, and expire when the worktree directory is removed (7-day backstop). Any session may release one at landing time.get_sessionsgains aworktreeslist, so in-flight worktree tasks stay visible after the session that started them is gone. - New
preflight_landingtool (worker-side, zero TD access): before porting a worktree diff, it intersects the landing's files with main-tree dirt (the classic blind-overwrite failure), peerfile:claims/touches, and unsaved live TDN state (tsvdirtycolumn), returning aclear/conflictsverdict with a reconcile hint. Dogfooded on its own landing (14/14 files correctly flagged against a known-truth scenario). MCP tool count: 53. - Worktree config mirroring hardened: the
-wt-config mirror now yields to worktree-native TD sandboxes (a worktree running its own instance keeps its own registry/config -- a workflow per-session pinning makes trustworthy), and re-mirrors on every registry refresh so worktrees created mid-session get config without an Envoy restart. - Undo-block guard self-heals: a begin/end pair severed mid-dispatch (extension reinit, crash) used to latch the re-entrancy guard and silently disable undo blocks for the whole session; a stale latch older than 60s is now reclaimed loudly.
- Test-procedure fix: the
/run-testscommand and skill now route through therun_testsMCP tool (deferred runner) --RunTestsSync()insideexecute_pythonran the whole suite inside that dispatch's undo block, failing the undo-guard tests and making the run one giant Ctrl+Z step. - Tests: 20 new (
TestBridgeInstancePinningx11,TestRegistryAdoptIfVacantx3,TestWorktreeCoordinationx6) covering pin resolution, epoch adoption, session-local vs all-sessions switching, registration adopt-if-vacant, durable-claim lifecycle, and landing-conflict computation. - Test suite: 104 suites, 2,357 tests.
v6.0.156¶
AI-guidance context overhaul: the heaviest always-loaded rules become slim invariants with their full recipes moving into the on-demand skills that load at point of use; Envoy tools carry their own prerequisites and schema-enforced enums; a five-reviewer line-level conflict audit across all rules and skills.
- Rules restructured to invariants + on-demand recipes (model-agnostic):
network-layout.md(13.9k -> 3.6k chars) keeps the layout invariants and hard gates while the full positioning recipe (spacing formulas, docked-companion slot patterns, panel-widget stacking, complexity thresholds, anti-patterns) moves into/create-operator, whose scope widens to ALL operator creation and movement (create_op,copy_op,set_op_position,execute_pythonbuilds).td-python.md(15.2k -> 10.0k) keeps the ironclad threading rule and summaries; the operator-referencing deep-dive, cook-model gotchas (feedback force-cook, Movie File In reload, animate-the-sampling), and the full coordinate table move into/td-api-reference.performance.md(9.7k -> 5.2k) keeps the gating protocol, stop conditions, and a dense safe-caps summary; the crash-cause and safe-default-caps tables move into/td-api-reference(new Heavy-Build Safety section). Nothing was deleted -- only relocated to load on task. All shipped templates mirrored; always-loaded context in the dev project drops ~50% (~104k -> ~52k chars). - Envoy tools carry their prerequisites and enumerate their values: 13 tool docstrings now state their required skill at point of use (
create_op/copy_op->/create-operator,create_annotation/set_annotation->/manage-annotations,execute_python/set_dat_content/edit_dat_content->/td-api-reference,externalize_op/save_externalization->/externalize-operator,create_extension,run_tests,get_sessions->/multi-session-etiquetteon_peers). Seven string parameters became schema-levelLiteralenums (set_parameter.mode,get_parameter.search_in,get_dat_content.format,create_annotation.mode,capture_top.format,get_docs.source,get_logs.level) -- documented values are unchanged, but invalid values are now rejected at the schema with the valid set listed, and MCP clients see the enum in the tool schema. - Line-level conflict audit (5 parallel reviewers, every finding independently verified):
glslMATdocks vertex/pixel/info -- not compute -- in the docked-companion enumeration (wrong hug targets in the Verify step); time-dependent ops are only flagged to cook, not always-cooking, in the crash-cause table (opposite diagnosis for "why isn't this cooking");td-api-referenceexamples no longer model forbidden/project1absolute paths (fromOP+ relative refs instead); CLAUDE.md's TDN blurb now says YAML v2.0 (legacy JSON read) instead of "JSON-based";run_testsgained its missing row in the/mcp-tools-referencecatalog;externalizations.tsvremoved from the commit checklist's never-stage list (it is machine-written and committed in 127 commits -- commit its changes, never hand-edit); the/run-testscommand now saves first and always readsdev/logs/(pass or fail);add-mcp-toolgained theEXPECTED_ENVOY_TOOLSsame-commit step;visual-aestheticsgauss-ease width unified to 0.2-0.5; staleproject.dirtyreferences corrected toproject.modified(the attribute does not exist on TD 2025 -- the destructive-test save-gate code already knew, the docs did not). - Dev-only guidance migrated to on-demand skills:
release-commits.md+github-release.md->/releaseskill,agent-tests.md->/agent-testsskill (description keeps the spends-subscription warning resident),td-ui.md->/build-uimechanics reference; root CLAUDE.md drops its derivable directory tree and duplicated reference lines.test_template_syncreads the sync table from the new/releaseskill path. - Smoke-harness hardening: the fresh-install bootstrap now fails LOUD when cleanup cannot delete a locked entry (a stale locked
.venvcorrupted the Envoy venv bootstrap on first run -- surfaced at run start instead of failing minutes later), and the ready-flag writes realscriptErrors()output instead of the method repr. Fresh-install smoke verified for this release: Embody loads clean, Envoy bootstraps and serves,UpdatestatusreadsDisabled, and the shippedEnvoyExtcarries the new prerequisites and enums. - Docs:
envoy/claude-code.mdrule/skill tables reflect the new invariants-vs-recipe split and wider/create-operatortrigger;testing.mdpoints at the/agent-testsskill path. - Known follow-up (filed from this release's smoke run): a freshly-registering Envoy instance claims the registry
activeslot and pulls every live session's bridge to it -- per-bridge instance pinning is the planned fix. - Test suite: 104 suites, 2,337 tests.
v6.0.154¶
A progress dialog for large TDN exports, plus two externalization-untag fixes.
- Chunked TDN exports show a progress dialog:
ExportNetworkAsync(the path behind the toolbar export button, the export keyboard shortcut, and whole-project TDN export) now opens a small centered progress window for large exports -- title, a live<comp> -- N / total operators (pct)status line, a progress bar, and a Cancel button. It auto-opens once an export covers >= 500 operators (show_progress=None, the default; existing callers get it for free) and stays out of the way below that. Cancel is consumed on the next batch boundary: no file is written and the worker unwinds cleanly. The export already ran batched across frames (200 ops/frame, now tunable viabatch_size), so TouchDesigner stays responsive throughout -- verified live against a 10,260-operator network (held 60fps) and a 3,060-operator content-heavy network that serialized to a 3 MB.tdn. A synchronous export of the same 10k network blocks the main thread ~1.5s; the chunked path spreads it with zero freeze. - No post-completion frame-drop burst: the async export's success hook stacked OS-window teardown, export tracking, and the manager-list force-cook/reset onto a single frame, cascading dropped frames for a beat after a large export finished. The list rebuild is now deferred off the completion frame so those costs no longer land together.
- TDN untag no longer leaves a ghost row:
remove_externalization_tagon a TDN-strategy COMP now routes through Embody's ownRemoveTDNEntry/RemoveListerRowhandlers instead of a raw tag-strip +Update(). TheUpdatesubtraction sweep deliberately excludes TDN COMPs (their lifecycle belongs toRemoveTDNEntry), so the old path left the externalizations-table row -- and the_tdn_rel_pathrecovery breadcrumb -- behind, a ghost the refresh sweep kept resurrecting.RemoveTDNEntrynow also clears that breadcrumb, and the MCP tool gained adelete_fileflag (default False -- agents untag non-destructively; the lister X button still deletes). externalize_opreports the right filename for TDN: tagging a COMP withtag_type='tdn'now returns the actual.tdnpath (from the tracking table) instead of a stale.toxname read offpar.externaltox.- Tests: new
test_tdn_export_progresssuite (6) covering the cancel request/consume path, the dialog guards, and the auto-show threshold; plus TDN-untag ghost-row +.tdn-filename regressions intest_mcp_externalization(+2) and breadcrumb/keep-file regressions intest_strategy_handlers(+2). - Test suite: 104 suites, 2,337 tests.
v6.0.153¶
The Envoy port scanner survives a zombie TouchDesigner holding a port.
- Zombie-held ports no longer wedge Envoy startup:
_findAvailablePortnow probes each candidate port with a realbind()instead of a TCPconnect(). A leftover / windowless TouchDesigner process can hold a port bound and LISTENING while its accept loop is dead -- connects to it are refused (so the old connect probe reported the port "free"), yet uvicorn's ownbind()then fails withWinError 10048, and the retry loop re-elected the same poisoned port for the full 30-minute restart window. Observed 2026-07-23: a second.toein the same repo crash-looped its Envoy because a windowless dev instance from hours earlier still camped port 9872. The bind probe performs the exact operation uvicorn will, so a dead listener can no longer fool it -- and it drops the 1s connect-timeout the old probe paid on every busy port. - Bind-failure blacklist (defense-in-depth): a port whose server worker dies before confirming a bind is recorded in
sys._envoy_bad_bind_portsfor 10 minutes and skipped by the scanner, so a probe/bind race (another process grabbing the port between the probe and the bind) advances to the next port instead of looping on the same one. A confirmed bind clears the entry, so a stale late error from an older server generation cannot poison a now-healthy port. - Tests: 6 new (
test_envoy_watchdog) -- a bound-but-dead zombie port is rejected in favor of the next free port, and the blacklist records on a pre-bind death, skips while fresh, expires after its TTL, and clears on a confirmed bind. - Test suite: 103 suites, 2,327 tests.
v6.0.152¶
Release hooks for portable exports, OpenCode as a first-class AI client, a setup-wizard git step, and a loopback-address sweep.
- Release hooks for Export Portable Tox (issue #74): Text DATs named
pre_release/post_releaseas direct children of the exported COMP automate the export.pre_releaseruns on a throwaway staged copy in/sys/quiet(the model used by AlphaMoonbase's Private Investigator) -- reset pars, delete scratch ops, all without touching the live component; a raise aborts the export and keeps the staged copy (renamed*_release_failed) for same-session inspection.post_releaseruns on the live original after the save -- even when the save failed -- with the resolved path and success flag (upload, notify, tag). Hook DATs are deleted from the copy, so hook code (and any credentials) never ships in the artifact; the staged copy is tag-neutralized and sync-disabled so hook edits can never write through to real source files.hook_mode='live'restores in-place semantics;run_hooks=Falseis the machinery flag (the self-updater's rollback backup uses it, and its return value is now verified). Exports whose target is -- or contains -- the live Embody COMP are never copy-staged. En route: a latentAttributeErroron DATs withfilebut nosyncfile(File In) is fixed, the export core is exception-contained end to end, the manager-UI export surfaces failures in a message box, and a failed dev release export now removes the stale release manifest. Newtest_release_hookssuite (28 tests). - OpenCode support: set AI Client to
opencode(or pick it in the wizard) and Embody generatesopencode.json-- anmcp.envoyentry spawning the same STDIO bridge Claude Code uses (meta-tools, cached tool list, reconnection), aninstructionsentry loading the generated.claude/rules/*.md, and apermissionblock matching your Tool Permissions posture on fresh files. Existing files are merged into, never overwritten (JSONC files are left untouched); the file is gitignored and mirrored into sibling worktrees like.mcp.json. Uninstall reverses the whole footprint shape-aware -- it stripsmcp.envoyplus the instructions entry from a mergedopencode.json(never injecting.mcp.jsonkeys into it) and removes an Embody-created one outright. Newtest_opencode_configsuite (12 tests). - Local Models & Open Clients docs page: OpenCode + LM Studio setup against Envoy, local-model recommendations, and the settings that make small models reliable -- context floor, quantization,
--jinja, and trimming Envoy's ~15-25K tokens of tool schemas to an 18-tool core via OpenCode'stoolsmap. - Setup wizard: git step: when no repo exists above the project, the wizard now asks -- Initialize Git (creates the repo first, so config lands inside it) or Skip for now -- instead of handling git silently; the step shows for every assistant choice including externalization-only. The git-init write runs under the wizard's bulk consent, so Advanced mode stays modal-free end to end. Wizard client list gains OpenCode. New wizard git tests (6).
localhost->127.0.0.1sweep: every shipped, generated, and machine-readable surface (ENVOY.md, llms.txt / llms-full.txt / for-ai.* in both web roots, docs) now targets127.0.0.1explicitly -- on Windows,localhostcan resolve to IPv6 first and stall every request (issue #57 class). Machine files also refreshed: tool count 53, current version, OpenCode config example.- Restore path knows OpenCode: the frame-30 config restore (
upgrade_envoy) now detects a missingopencode.json/.claude/rulesfor OpenCode users, matching the other clients. - Fresh installs survive cloud-poisoned uv caches: Envoy's dependency install now runs uv with
UV_LINK_MODE=copy. uv's default hardlink strategy fails machine-wide withos error 396once any venv on the machine hardlinked a cache entry inside a Dropbox/OneDrive-synced folder (the Windows cloud-files filter rejects new hardlinks to those inodes) -- fresh installs then died with "Python environment not ready". Caught by the v151 fresh-install smoke; copy mode trades a few MB of one-time disk for immunity. - Release All (issue #74 follow-up):
op.Embody.ReleaseAll()-- or the new Release All pulse on the Embody page -- exports every releasable component as its own portable.toxin one pass. Releasable = Embody-tracked AND hook-bearing; hooks alone never qualify, because third-party components ship with their authors' hook DATs baked in (verified in the wild). Per-component failures log and skip without halting the batch; duplicate names disambiguate with a suffix. - Show Built-in Pars toggle (issue #77): a new Advanced-page toggle unhides TouchDesigner's built-in parameter pages (Layout/Panel/Look/Common/...) alongside Embody's -- e.g. to reach the Common page's Global OP Shortcut. Off by default; applies live and survives reinit (the v6.0.148 custom-pages-only filter now converges on your choice instead of forcing it).
- Test suite: 103 suites, 2,321 tests.
v6.0.149¶
Update controls live with the version info.
- Auto-Update moved to the About page:
Autoupdate,Checkforupdate, andUpdatestatusnow sit under the version/build info on About (behind a section break), where update controls belong -- Advanced returns to its focused table/cleanup/safety shape. Values, persistence, and the live status behavior are unchanged; the Parameter Reference regroups accordingly.
v6.0.148¶
The parameter dialog shows only Embody's pages.
- Custom-pages-only parameter dialog (the POPX pattern):
showCustomOnlyis now set on the Embody COMP, so its parameter dialog shows the 9 Embody pages (Embody, Tags, TDN, Envoy, Logs, UI, Shortcuts, Advanced, About) instead of those plus TD's built-in Layout/Panel/Look/Children/Drag-Drop/Extensions/Common. The built-in pages stay fully functional and reachable -- the flag is a dialog filter, toggled back trivially. Applied inEmbodyExt.__init__, so existing installs converge on it after the update without re-authoring. Newtest_component_presentationsuite. - Parameter Reference is truth-synced: the Auto-Update parameter descriptions now live in the component's parameter help (the generated
docs/embody/parameters.mdregenerates fromEmbody.tdn, so hand-edits there were doomed to revert);Update StatuscarriesDisabledas its authored default, matching the enforced resting state. - Test suite: 101 suites, 2,225 tests.
v6.0.147¶
The update prompt is a decision, not a reading assignment.
- Update-available dialog trimmed: just the version pair (
Update available: v6.0.148 (installed: v6.0.147)), a link to the release notes on GitHub, and Install / Not Now. Previously the dialog embedded up to 600 characters of the release-notes body (project intro, changelog bullets) -- overwhelming for a yes/no choice. The dialog is rendered by the installed updater, so this appears from the first check made on v6.0.147+.
v6.0.146¶
Update Status always tells the truth -- caught by a fresh-install smoke of the shipped v6.0.145 .tox.
Update Statusis never blank: the read-only status line now rests atDisabledwhenever Auto-Update is Off -- on a fresh install (v6.0.145 shipped an empty field, which reads as broken), on every project open (also replacing a stale "vX available" left by a session that had checks enabled), and the moment the preference is flipped Off. Leaving Off clears it so the next check (startup, or a Check for Update pulse) writes the real state. The fresh-install initializer writes through the read-only dance -- direct assignment to a locked parameter is not reliable.- Release procedure hardened: a fresh-install smoke of the shipped
.toxin a virgin project is now a mandatory release step -- a cold open of the dev project exercises a different path than a first install, which is exactly how the empty field slipped through. - Test suite: 100 suites, 2,223 tests (3 new Update Status resting-state tests).
v6.0.145¶
Annotation integrity across the whole TDN pipeline (external bug report: double-serialized annotate subtrees, gutted widget internals on cold open, resurrecting deletions, unresolvable annotation paths), plus in-place self-update. (v6.0.142-144 were consumed by saves during the dev cycle and never shipped.)
- Annotations are never externalized per-op (external report, all four issues verified then fixed): a code-created annotation was an ordinary COMP subtree to Embody's sweeps, so Externalize Project,
Tdncascade, and the save-time content-safety sweep could tag its TD-managed widget internals as their own TDN/source boundaries -- double-serializing the widget beside the parent's semanticannotations:entry, gutting its internals on cold-open reconstruction (emptycolortable ->float(None)cook errors on the stock expressions), and stranding orphan files that stale cleanup never removes. Tagging now refuses annotates and their interiors at every layer: theapplyTagToOperatorchokepoint, the project-sweep filter, the cascade, both at-risk sweeps (DATs and storage), the tagger UI's add path, andexternalize_op(clear refusal message). Legacy rows pointing at or inside an annotation are inert -- skipped by the row enumerator with per-row re-checks during cold-open reconstruction and auto-save recovery -- and clean up fully viadelete_opor the manager (removal paths deliberately unguarded). create_annotationnow createsutility=True(TD-UI parity): MCP-created annotations behave exactly like hand-drawn ones. Verified live on TD 2025.33070: a bare Python create isutility=False,op()hides a utility node but resolves paths through it, and deepfindChildrendoes not descend into a utility annotate withoutincludeUtility=True-- so new annotations are structurally invisible to every enumeration sweep, closing the bug class at the source.- Every op-path Envoy tool resolves utility annotations: a shared utility-aware resolver (
resolve_op: bareop()fast path, then a root-downincludeUtilitywalk) now backs ~34 tools --delete_op,set_parameter,get_parameter,get_op,set_op_position,query_network,get_network_layout,cook_op, batches, and the rest. Previously onlyset_annotation/get_enclosed_opscould resolve whatget_annotationshad just listed;delete_opanswering "Operator not found" on a listed annotation is what pushed agents to raw.destroy()in the first place. The not-found recovery hint now points atinclude_utility. - Annotation deletion is durable:
delete_opresolves the annotation, purges any tracking rows and files, and the auto-save checkpoint re-exports the parent's.tdnwithout the semantic entry -- a live-deleted annotation can no longer resurrect from a staleannotations:entry on the next reimport or cold open. Additive (clear_first=False) imports reuse an existing utility annotation by name instead of duplicating it. New rule in the manage-annotations skill (+ shipped template): delete viadelete_op, never raw.destroy(). - Self-update (Auto-Update): Embody checks GitHub for new releases and updates itself in place -- no manual download, settings and externalizations preserved. Manifest-gated: the dev save hook now writes
release/embody-release.json(version, tag, asset, byte size, SHA-256, TD-build floor) beside the exported.tox; the updater refuses TD builds belowmin_td_buildbefore downloading, verifies the digest after, backs up the installed version, and rolls back on a failed install. New Advanced-page parameters:Autoupdate(Off / Check and Notify / Check and Install -- default Off, persisted),Checkforupdate(pulse),Updatestatus(read-only). Docs: embody/auto-update.md. Newtest_updatersuite (17 tests). - Destructive-test save-gate actually fires:
project.dirtydoes not exist on TD 2025, so theRunDestructiveTestsunsaved-changes refusal silently never triggered; it now checksproject.modified. - worktree-td-safety rebalanced (rule + shipped template): live-tree editing is the default for a sole writer; isolated worktrees are for genuinely risky landings (multi-extension broken intermediates, concurrent writers) -- matching isolation to risk instead of a blanket mandate.
- Adversarial 5-lens review panel on the annotation diff (correctness, legacy-cleanup, blast-radius, spec-fidelity, perf/noise); every finding fixed and re-verified, including the annotate-leaf legacy-row gap and a cold-open re-check the initial fix missed. Fresh-save + full TD restart cold-open smoke: startup restore clean, zero project errors, all annotation suites green on the cold instance, Tier-1 MCP agent contract PASS.
- Test suite: 100 suites, 2,220 tests (32 new annotation tests including the
test_annotation_guardssuite; 17 updater tests).
v6.0.141¶
Issue #57's create_op freeze fixed at its trigger (Embot viz activation gates), TDN warning and dirty-state quality-of-life from two landed worktrees, and a UTF-8-safe .tdn git diff driver. (v6.0.139/140 were consumed by saves during the dev cycle and never shipped.)
- Issue #57 -- MCP
create_opcould permanently freeze TD (viz activation gates): on a reporter's TD 2025.32460, the first mutating MCP call of a session reproducibly wedged TD's main thread (Windows AppHang 1002; dumps showed an orphaned/self-owned critical section inside TD's editor internals with the GIL held, so the MCP response could never be delivered). The reporter's A/B testing pinned the trigger to the build-visualization performing editor work -- bot template creation, annotateCOMP copyOPs, selection writes, pane navigation -- in the SAME frame as the network mutation, on the first activation after dormancy. Two frame-arithmetic gates now decouple those moments: a settle gate (no viz editor work within 2 frames of any mutating op, so the MCP response is always delivered first) and a cold hold (the first hop after viz dormancy pings the node colour only; bot/camera machinery engages ~0.5s later, after the editor has finished rendering the new op). TurningEmbotenableandEnvoyfollowoff remains a complete workaround on affected hosts. New suitetest_envoy_viz_gates(9 tests); 4-reviewer adversarial panel found no defects. - TDN locked-content warnings no longer storm: a full-project externalization shows ONE combined dialog listing every COMP with locked TOP/CHOP/SOP data (frozen pixels/channels/geometry that cannot survive TDN), instead of one modal per COMP. The dialog adds a Don't-show-again opt-out persisted to the new Locked Content Warning (
Tdnlockedwarn, Ask/Quiet) preference; the log WARNING always fires regardless. 7 new tests intest_tdn_helpers. - Dirty (unsaved) badges no longer vanish after an extension source edit: the TDN fingerprint baseline cache moved from an instance attribute -- which every syncfile-triggered extension reinit wiped, so the next sweep re-baselined unsaved COMPs as clean (13 dirty badges silently cleared in the field) -- to ownerComp storage that survives reinit; it is excluded from TDN export and cleared at project open. New
TestTDNFingerprintPersistencetests. - Manager filter: new
dirtykeyword + filter force-expand:dirtyshows only rows with unsaved in-TD changes (complementingchanged= unsaved OR git-uncommitted, via a single sharedrow_is_unsaved()source of truth); while any filter is active, parent branches force-expand so matches under collapsed parents stay visible, and your expand/collapse state is restored when the filter clears. Tagger buttons relabeled for the conversion flow (Convert to tox,Remove tdn). - Storage hygiene -- runtime state stays out of .tdn files:
git_status,expand_order,_tdn_fingerprints, and_suppress_dialogsare now excluded from TDN export. The last closes a save-window trap:project.save()stores_suppress_dialogs=Truefor the duration of the save and the TDN export runs inside that window, so every save baked it intoEmbody.tdn-- and a later TDN restore would have suppressed dialogs for the whole session (caught pre-commit on the v6.0.140 save). - .tdn git diffs survive unicode: the textconv diff driver now reconfigures stdout to UTF-8. Non-ASCII network content (button-label glyphs, annotations) crashed
git diffon .tdn files under Windows' cp1252 console codepage; fixed in the dev source, the shipping template, and the deployed driver. - Test suite: 98 suites, 2,171 tests.
v6.0.138¶
New shipped skill /brief -- a task-brief compiler that turns a conversational request into an executable contract -- plus Launch AI Client per-OS install walkthroughs and a closed template-shipping test gap. (v6.0.137 was consumed by a save during the dev cycle and never shipped.)
/brief-- task-brief compiler (new shipped skill): user-invoked (/brief <request>, or bare/briefto formalize the conversation's current ask); compiles plain conversational English into a reviewable brief inbriefs/(gitignored -- new managed.gitignoreentry): the skills to load before which tool calls, live-discovered anchors (op.Embody.parent().path, never guessed paths), verifiable-only success criteria (captured-and-assessed frames, cleanget_op_errors, fps within tolerance of baseline), and gates (performance baseline,claim_scopebefore big steps, the worktree rule for externalized-file work). Execution then follows the brief as a contract -- portable to sub-agents and fresh sessions, which never see the conversation -- with deviations recorded in the brief and fed back into the skill. Ships as the 14th skill template; the generated user-projectCLAUDE.mdgains a Task Briefs section; the docs skills table adds/briefplus five previously-undocumented shipped skills (/pop-networks,/movie-export,/parameter-design,/td-recovery,/multi-session-etiquette).- Launch AI Client: a missing CLI now walks you through the install: terminal CLIs (Claude Code, Codex, Gemini) carry per-OS install specs -- the opened terminal prints numbered steps with the official install command for the current OS on its own copy/paste line, plus a labeled alternative and the docs link, instead of the old one-line hint. Rendering is shell-correct by construction: zsh uses
printf '%s\n'(zsh's echo interprets backslash escapes even in single quotes), cmd.exe usesecho(with quote-aware^-escaping, doubled%, andsetlocal DisableDelayedExpansion. Failure dialogs reuse the same per-OS summary viainstall_summary(); the Windows CLI probe learns Codex's native install dir (%LOCALAPPDATA%\Programs\OpenAI\Codex\bin).test_launch_aiclientgrows 29 -> 42 tests (escaping, per-OS rendering, summary selection). - Template shipping gap closed: config deployment reads template DATs from the live network and silently skips missing ones -- so a
_TEMPLATE_MAP_*entry whose DAT was never created in thetemplatesCOMP passed the entire sync suite (disk-file checks only) while shipping nothing. Newtest_F01_map_entries_resolve_to_live_datsfails on any mapped template missing or empty in the live COMP. Caught by the adversarial review of this release's own feature. - TDN drift catch-up: the tagger
.tdnfiles re-exported for the first time since v6.0.25 -- on-disk labels now match the live UI (and drop the non-ASCII glyphs that crashed the.tdngit textconv under cp1252);templates.tdn's color type-default restored. - Tests: full normal-tier suite at release: 2,142 passed / 0 failed / 7 platform-conditional skips (93 suites / 2,149 tests; the 3 agent-tier and 1 destructive-tier suites are excluded from normal runs by design). Agent Tier-1 MCP contract test: PASS (bridge spawn from
.mcp.json, handshake, tool-inventory match, curated call sequence).
v6.0.136¶
TD 2025 external-tox reload triggers fixed: restores, reloads, and reconciles actually load from disk again, and a failed load is loud instead of silent. Root-caused during the stale-tox-restore investigation, which also empirically established that on TD 2025 builds the externalized FILE wins over a tox-embedded DAT snapshot in every load path.
enableexternaltoxpulseis the load trigger now: TD 2025 lacks every affordance the tox reload paths relied on --reloadtoxpulsedoes not exist (theReconcileMetadatatox branch raisedtdAttributeErrorand aborted the whole reconcile pass), togglingenableexternaltoxoff->on does not re-read the.tox(the manager's "Reload from disk" was a silent no-op that still logged SUCCESS), and settingexternaltox+enableexternaltoxon a fresh COMP does not auto-load mid-session (RestoreTOXCompsrestored permanently EMPTY shells; a laterSave()could then export an empty.toxover the good file). All three paths now pulseenableexternaltoxpulse-- the explicit, synchronous load trigger, verified with headless probe projects on 2025.32820 AND 2025.33070.- Fail-loud restore with a real success signal: a failed
.toxload posts NO exception and NO script error on TD 2025, soRestoreTOXCompsnow verifies viaexternalTimeStamp-- it stays 0 when nothing loaded, while a valid-but-EMPTY.toxstill stamps it (no false positives on placeholder containers). A dead shell is logged at ERROR and destroyed so the next startup can retry. Cook-timing-deferred loads get a deferred verification pass (_verifyTOXRestoreLoaded) that completes the restore -- tag, color, position -- only after the load lands, because a pulse reload wipes tags not saved in the.tox(verified empirically). - Reconcile hardening:
ReconcileMetadataguards each row (one broken row logs an ERROR and the pass continues -- previously it aborted), refuses to pulse when the.toxis missing on disk (a blind pulse is a silent no-op that would count the row reconciled), writes pars before tags (wrongly-typed rows no longer accumulate stray tags), re-tags AFTER the reload, and the summary reports failed-row counts at WARNING. - Stale-tox-restore report: not reproducible on TD 2025: the field report of tox-embedded DAT snapshots resurrecting over newer externalized
.pyfiles (then clobbering them at save) does NOT reproduce on 2025.32820 or 2025.33070 -- eight headless probe runs confirmed the externalized file wins in every load path (.toe-open single and nested,reinitnet,enableexternaltoxpulse), with no startup race window (the file re-read happens during.toeload itself, before any script can run). Older TD builds exhibited the clobber in the field (2023 forum reports), so the report's Embody-side hardening suggestions (content-compare on launch, DAT->file write-back logging) stay on the roadmap. A repo-wide sweep of 90externaltoxsites found no other code relying on the set-triggers-load assumption;ExportPortableTox's restore phase now documents that it depends on set-does-not-load semantics. - Tests: new
TestTOXRestorationsuite intest_dat_restoration(6 tests: pulse-par contract on container/base COMPs, restore-loads-content, unloadable-tox-fails-loud, empty-tox-is-not-an-error, reload-rereads-disk, reconcile-row-guard). Fresh-install smoke-tested from the shipped.toxon a clean TD instance, all three fixed paths exercised in the shipped build. Full suite at release: 2,123 passed / 5 environmental clipboard failures / 7 platform skips (97 suites / 2,135 tests). The five failures are clipboard-lock contention, not regressions: three concurrent TD instances each run the 1.5s clipboard watcher and Windows clipboard reads fail intermittently under lock contention -- two consecutive same-frameui.clipboardreads were observed disagreeing, the affected test re-passed 3/3 in a quiet window, and the diff touches no clipboard code. Follow-up: retry-on-lock hardening for Embody's clipboard reads.
v6.0.135¶
Upgrade path made non-destructive and freeze-free: the Skip/Re-scan dialog is gone -- dropping a new Embody .tox into an existing project now validates tracked operators quietly instead of deleting and re-exporting every externalized file in one frozen frame.
- Upgrade freeze fixed at the root: the old dialog's "Re-scan" button (labeled "Recommended after upgrading Embody") called
Reset()--Disable()synchronously unlinked EVERY tracked file (bypassing theFilecleanuppreference), thenUpdate()re-externalized the entire project in ONE synchronous main-thread frame: per TOX COMP a full.toxserialization, per TDN COMP a fullExportNetworkplus a ~700ms whole-project stale-file rglob scan. On real projects that froze TD for minutes -- and force-killing the frozen TD landed in a crash window with zero externalized files on disk (recovery only via git or the.toe's embedded copies; TDN COMPs stripped at last save could be lost outright).Verify()'s upgrade branch now calls a new_validateTrackedOperators()helper instead: log the tracked-row count, clear Embody's own drag-sourceexternaltox, and defer the standardUpdateHandler()pass -- schema migration, path normalization, per-row continuity validation, stray-tag pickup, dirty-only re-export. No dialog, no deletion, no freeze. - Skip-path gap closed: clicking Skip used to leave the table schema migration and path normalization never run (both lived only behind the Re-scan branch). The always-run quiet validation now covers them on every upgrade.
- Destructive rebuild is disclosure-only now: the one-click undisclosed file-deletion path is gone. A genuine ground-up rebuild remains available via Disable -> Enable, whose dialog states that files will be deleted.
Reset()stays as public API but no longer has production callers. - Minimum TD build floor is now 2025.33070: this release was saved on the current official build; the automated version-doc sync (README, docs, CONTRIBUTING) raises the support floor accordingly.
- Docs: README and getting-started now describe the quiet validation; the smoke-harness seeding comment updated (the
'Embody': 0auto-response remains for the duplicate-instance dialog, which shares the title). - Tests: new
test_verify_upgradesuite (3 tests) pins the non-destructive contract -- no file deletion, no table clears, no Status flip, plus source-level tombstones that fail if the dialog or aReset()/Disable()call ever returns to the upgrade path. Full suite at release: 2,122 passed / 0 failed / 7 platform skips (97 suites / 2,129 tests).
v6.0.134¶
TD 2025.33070 palette-scan freeze + frame drops, fixed structurally: the palette scan no longer loads components into TD at all -- a background toeexpand worker reads types and child counts from unpacked .tox files with zero main-thread cost -- plus 33070 bootstrap rows, a poison-pill sentinel, and blocklist entries for the two components that wedge 33070. (v6.0.132/133 were consumed by saves during the fix cycle and never shipped.)
- Palette scan rebuilt on
toeexpand-- zero frame drops, zero freeze surface: on a bootstrap miss the scan now expands each palette.toxwith TD's bundledbin/toeexpandon a WORKER thread (pure subprocess + file I/O, no TD access) and reads the placed component's type and child count from the expansion (both toeexpand output formats: new-style.ntrees and old-style.inittrees liketemplate.tox); arun()-chain poller drains results on the main thread and reuses the existing checkpoint/resume machinery. Parser validated against loadTox-derived ground truth for every comparable 33070 component. Measured on the old path: 78 of the first 91 palette loads exceeded the 60fps frame budget (6.3s of main-threadloadToxin one-third of the scan). The new path does no per-component main-thread work, and since no palette component's init code ever executes, the entire class of load-time freezes cannot occur. Guardrails: a scan whose expansions ALL fail (e.g. toeexpand blocked by AV policy) falls back to the in-TD scan instead of finalizing an empty catalog; a dying poller cancels the worker via a stop event; stale worker temp dirs are swept at scan start. The legacy in-TDloadToxscan survives only as a fallback when toeexpand is missing, now hardened withallowCooking=Falseon the scan wrapper (palette per-frame executors can no longer fire during the census) and the freeze sentinel below. Note:toeexpandreports success with exit code 1 -- outcomes are judged by the expansion directory, never the return code. - geoPanel and chromaKey blocklisted -- the TD 2025.33070 wedge : loading
Techniques/geoPanel.toxcan wedge TouchDesigner 2025.33070's frame loop within 1-2 frames ofloadToxRETURNING (the process stays alive and "Responding" but no frame ever advances -- UI, run() chains, and delayed callbacks all stop). The wedge is environment-dependent, not deterministic: 6 of 7 test runs wedged -- including an isolation .toe containing NO Embody code at all, which rules out Embody as the cause -- while one real-project full scan on 33070 completed, so a timing/UI-state race in TD decides it. The md5-identical file always loads clean on 2025.32820; reported upstream to Derivative. geoPanel ships per-framepanel.interactTouch()/interactMouse()/setFocus()Execute-DAT callbacks that fire the moment the network exists, plus Leap Motion SDK init and a hard-coded LAN Touch In.Tools/chromaKey.toxwedges 33070 the same way (full-palette probe with geoPanel excluded stalled right after it). On a fresh 33070 install the scan froze atScanning palette (89/251); because checkpoint-resume (v6.0.128) resumed straight back into the same component, every relaunch froze again. - In-flight sentinel convicts freeze-causing components automatically: the (fallback) scan writes
.embody/palette_scan_inflight.jsonnaming each palette component right before loading it, removing it only on clean outcomes (scan finalize, graceful abort, extension teardown). A launch that finds a sentinel knows the previous session was killed or wedged mid-scan, permanently skips the named component for that build (persisted under the reserved_palette_blockedcatalog key, carried through checkpoints), and logs what happened. Any future freezing palette component self-heals on relaunch instead of freeze-looping -- the sentinel survives until the NEXT component's write, and the fallback scan now spaces chunks 3 frames apart so wedges landing 1-2 frames afterloadToxreturns (exactly geoPanel's failure mode) are attributed to the right component, not its innocent successor. - Bootstrap palette rows for TD 2025.33070:
palette_catalognow ships rows for build 099.2025.33070 (extracted via toeexpand from the 33070 palette, including the new POPs Gaussian Splats components and both wedge-causing components -- toeexpand executes nothing, so even those are safely cataloged), so fresh installs on the current official TD build skip the runtime scan entirely. - Save-wedge regression caught and fixed before shipping: the sentinel's first iteration cleared itself in
onDestroyTDviaext.Embody._findProjectRoot()-- andExportPortableTox's file-ref strip reinitializes CatalogManagerExt mid-project.save(), so the teardown's cross-extension call wedged the save (zero CPU, 0-byte.toe, twice). Teardown is now inert unless the session actually wrote a sentinel (only a legacy loadTox scan does), and clearing uses a path cached at write time -- zero file I/O and zero extension lookups on a normal teardown. Side benefit: a session that wrote nothing can no longer delete a sibling instance's live sentinel. - Release-procedure note: externalized files changed on disk while TD was closed (a landed worktree diff) are only re-synced into their DATs by the post-launch refresh sweep -- which can run AFTER an early
project.save(), exporting a stale portable.tox(observed: the palette_catalog table missing its new build rows). Verify affected DATs re-synced before a release save;release-commits.mdnow documents this. - Tests: new
TestCatalogPaletteSentinel(10 tests) andTestCatalogToeexpandScan(9 tests) suites cover the sentinel round-trip and conviction flow, the teardown guard (own-sentinel-only clearing), the expansion parser (type mapping, child census, ambiguity rejection), a real end-to-end toeexpand worker run, scan routing, and the poller's drain/finalize/fatal-fallback paths. Full suite at release: 2,117 passed / 1 environmental clipboard flake (re-passed in isolation) / 7 platform skips.
v6.0.131¶
Multi-instance bridge correctness (issue #57 follow-up): instance-aware process liveness, restart_td can no longer quit the wrong TouchDesigner, plus a Windows TDN-rename file leak and the pre-existing Windows test failures fixed.
- Bridge liveness is instance-aware: the bridge inferred "TouchDesigner is running" from ANY TD process on the machine (
find_td_pid()= first process matching the image name). With several projects open, a dead instance read as alive,crash_detectednever fired, andlaunch_tdrefused to relaunch. Liveness is now: the active instance's REGISTERED pid (image-verified viais_td_process_alive, so an OS-recycled pid cannot false-match) or its registered port answering. A dead registered pid resolves toNone-- never to a stranger's pid.TouchDesignerWebRenderhelpers are excluded from process discovery on Windows. restart_tdtargets only the active instance: it previously quitfind_td_pid()-- literally the first TouchDesigner on the machine, which with multiple projects open could terminate a DIFFERENT project's TD. It now resolves the active instance's verified pid from the registry (state fallback, also verified) and refuses with a clear message when that instance isn't running, ignoring unrelated TD processes.- TDN rename no longer leaks the old file on Windows :
_updateMovedTDNOpusedPath.rename(), which overwrites on macOS but raisesFileExistsErroron Windows when Embody's own sweep already exported the new-name.tdn-- every rename left the old file behind (Error renaming TDN filein logs). NowPath.replace()for identical cross-platform overwrite semantics. - Launch scripts normalize to forward slashes:
str(Path)flips to backslashes on Windows hosts, producing invalid zshcdpaths in the macOS.commandscript and platform-dependent.batcontent. Both generators (and the invoked CLI path) now emit forward slashes; verifiedcmd.exeaccepts quoted forward-slash paths forcd /dand program invocation. - Windows test-suite health:
test_tdn_crash_safety.test_A04skips on Windows (chmod cannot write-protect a directory there);test_resolve_toe_path_relativebuilds its expectation portably (abspath adds a drive on Windows); watchdog tests no longer signal the LIVE server's shutdown event (a full-run server bounce); the stuck-start revive test pins an expired startup deadline; and thetest_shortcutsparexec tests pin the parexec suppression gate open (_restoring_settings/_init_completeare live shared state other suites toggle mid-run -- the order-dependency that made them fail only in full runs). Full suite on Windows: 2085/2092 passed, 0 failed, 7 skipped (the skips are the Unix-only pgrep tests plus the Windows chmod skip).
v6.0.130¶
Windows MCP transport fix (issue #57): bridge targets 127.0.0.1, restart-storm guards, delete_op tracking purge -- plus the Envoy watchdog false-revive fix and MCP test-runner status hardening (issue #60 follow-up).
- Bridge targets
127.0.0.1, neverlocalhost(fixes #57): the STDIO bridge forwarded every request tohttp://localhost:<port>/mcp. Windows resolveslocalhostto::1first, Envoy binds IPv4-only, andurllibtries addresses sequentially -- on hosts whose firewall stealth-drops loopback SYNs (measured: ~2.0s to refuse a closed loopback port that healthy Windows refuses in <1ms), every single MCP call burned ~2s on the doomed IPv6 attempt, and a full drop becomes the reported multi-minute "create_op hangs" (the bridge forward timeout is 300s). All bridge URL sites, theenvoy_setupHTTP-fallback.mcp.json, and the disk-fallback bridge copy now target127.0.0.1explicitly. Measured on the reporter's host class: 2.1s -> 0.07-0.27s per call. - Envoy no longer restart-storms when its port is contested: a first startup failure with the configured base port held by another process entered a self-sustaining loop (observed: 575 attempts over 30 minutes, continuing even after "Envoy disabled") -- stacked restart
run()s all eventually fired, the watchdog revive cleared the duplicate-start guard and signaled the in-flight worker's shutdown event, and_forceCloseOldServerkilled newborn servers via the global handle with no ownership check. Now:Start()gates onEnvoyenable; queued restarts are generation-stamped and go stale when superseded; the revive skips starts inside their startup window; force-close verifies the handle's generation before signaling or closing anything; and a worker that finds its shutdown event pre-set at startup logs a loud dead-on-arrival WARNING with generation/lifetime detail instead of looping silently. Verified live: contested base port now recovers in one clean scan-and-bind. delete_oppurges tracking for every strategy: deleting an externalized DAT (or tox/json/... op) left itsexternalizations.tsvrow and on-disk file behind until a Refresh sweep reclaimed the row (the file never)._purgeTDNTrackingis now_purgeExternalizationTracking: rows for the op and tracked descendants are removed synchronously for all strategies, files are deleted on the same deferred schedule as the TDN path -- guarded by the clone-tag and shared-file-reference checks (_checkFileReferences) so a file another live op still uses is preserved.- Bridge
tools/listaugmentation is idempotent: the shipped bridge template appended its meta-tools (get_td_status,launch_td, ...) without checking for duplicates -- the guard existed only in the repo's disk-fallback copy, which had silently drifted ahead of the template that actually deploys. The guard is forward-ported to the template and the two copies are re-synced byte-identical. - Watchdog no longer revives a healthy cold start: the liveness watchdog treated
Preparing Python environment...(the fast-path import gate warming the MCP Python stack on a worker thread) as a settled state, probed the not-yet-bound socket, and force-revived the in-flight startup ~8s in -- observed 7 seconds after launch on a cold open. The status is now classified as transitional, so a slow first import gets the same ~24s stuck-grace asStarting.../Restarting.../Reviving..., and a genuinely orphaned import gate (extension reinit mid-warmup) still self-heals via the grace-path restart. - Overlapping
run_testscalls are refused: a second MCPrun_testswhile one was active capturedTestingas the "prior" Embody Status and restored that lie after the run (Status stuck atTestingforever), while overwriting the first caller's completion handle (30s transport timeout). The tool now refuses overlapping runs cleanly, keeps the prior Status in COMP storage so it survives an extension reinit mid-run, never captures the literalTesting, and the completion poll restores Status even when the pending handle was lost. test_smoke_release.test_status_enabledfixed: it read the live Status par, which the MCP runner holds atTestingfor the entire run -- a deterministic failure on every MCP-invoked run (misread as a revive race during the v6.0.128 release run). It now asserts against the saved prior status; a genuinely stuckTestingstill fails loud. Watchdog suite +5 tests:Preparingtransitional classification, stuck-gate grace restart, and the storage-backed status-restore contract.
v6.0.128¶
Issue #60 (default-startup-file freezes, timeline fighting, prompt nagging): five root-cause fixes across the catalog scan, the dropped-.tox sweep, settings persistence, and the Envoy venv probe -- plus a new shipped worktree-td-safety rule.
- Palette scan stops fighting the user's timeline (fixes #60): the first-launch catalog scan snapshotted timeline state once at scan start and force-restored it after every chunk, so pausing mid-scan was un-paused over and over. The snapshot/restore bracket is now per-chunk: a user pause (or rate/cookRate/realTime change) between chunks is adopted, while a palette component's own mutations inside the chunk are still undone.
- Catalog scan checkpoints and resumes: the catalog was only written at the very end of the full scan, so closing TD mid-scan restarted it from zero on every launch, forever. The op-type half is now written before the palette phase, palette results checkpoint every 25 components (
_palette_partialmarker), writes are atomic (tmp +os.replace), and the next launch resumes where the scan left off (deferred past the frame 30-90 restore phases). A scan can no longer wedge itself (_scan_in_flightclears on failure), silently re-enable a Disabled Embody (_setScanStatusguard), or crash the cross-build patcher on a checkpointed catalog (_findShiftedDefaultsskips reserved keys). tdn_excludesilences the dropped-.tox sweep, ancestry-wide (fixes #60): the "Dropped .tox Expression Detected" dialog never consulted the exclude tag; it now skips tagged COMPs and their whole subtrees (_hasExcludeTagInAncestry), as does Externalize Full Project (_shouldSkipOp). A plain Ignore is remembered for the session;Toxdropexpris persisted to config.json so "Always" answers survive into new untitled projects (which reload baked.toedefaults and previously re-prompted every time). The Envoy opt-in prompt likewise honors a restored config instead of re-asking per project.- Venv probe hardened: the synchronous venv-python probe ran on every
Start()including every watchdog revive (recurring main-thread stall) and a probe timeout deleted the venv. It now probes once per session per venv path, a timeout falls back to system Python without touching the venv, timeout dropped 10s to 5s, and the probe no longer flashes a console window on Windows. Test-runner-suppressed parameter values no longer persist to config.json mid-run. - New shipped rule
worktree-td-safety: multi-step edits to externalized files belong in a git worktree; landing into the main tree wants TD closed (syncfile hot-reload), with a bidirectional drift check before porting. Ships to user projects via_TEMPLATE_MAP_RULES. - 92 suites / 2,090+ tests; 42 new/updated tests across the palette-scan, toxdrop, and settings-persistence suites. Known pre-existing failures on Windows (path-separator, shell-quoting,
chmodno-op read-only-dir) are unrelated and tracked separately.
v6.0.126¶
Two field-reported fixes (both from benjavides): the TDN save-time locked-content warning no longer fires for locked ops a nested externalization boundary already preserves (issue #53), and non-file-backed DATs no longer crash the externalization refresh sweep (issue #54). Fresh-install smoke-tested from the shipped .tox, which caught and fixed a residual issue-#54 sibling in the removal cleanup.
- Locked-content warning respects nested externalization boundaries (fixes #53):
_warnLockedNonDATsscanned the whole subtree withfindChildren(), so a locked TOP inside a nested TOX-strategy child COMP popped the "Locked Content Warning" on every save of the TDN parent -- even though the child's own.toxpreserves that locked data fine, and the dialog's "switch this COMP to TOX" advice named the wrong COMP. The scan (extracted into a testable_findLockedNonDATs) now skips operators below any nested boundary the exporter itself skips -- a tox- or tdn-tagged child COMP (exported separately as atox_ref/tdn_refpointer) or an exclude-tagged subtree -- mirroring_collectAllPaths. A nested TDN child still raises its own warning when it exports itself, so nothing is silently lost. Docs updated (externalization guide + TDN spec). - Non-file-backed DATs no longer crash the refresh sweep (fixes #54):
getExternalPath()assumed every DAT has afileparameter, but selectDAT/mergeDAT and friends don't -- and a tracked path can come to resolve to one after a delete/rename swap, at which pointupdateDirtyStateskilled the wholeRefresh()withtd.tdAttributeError.getExternalPathnow returns''for non-file-backed DATs (socheckOpsForContinuityclassifies the row as "replaced" and routes it through the existing recovery prompt instead of crashing),setExternalPathrefuses them with a WARNING log, and the dirty-state sweep skips them without blanking the table'srel_file_pathrecovery pointer. Tag-time discovery already excluded them (parName='file'filter +supported_dat_types), so the guards close the stale-table-row gap, not a tagging gap. - Removal cleanup survives non-file-backed DATs (found by the fresh-install smoke test of the fix above):
RemoveListerRowclearedsyncfile/fileunconditionally on its DAT branch, so removing the tracking row for a type-swapped DAT raised a caught-but-loggedAttributeErrorthat aborted the color reset and parameter-tracker removal mid-cleanup. The par-clearing is now gated on the DAT actually having afileparameter. - 92 suites / 2,090 tests (+10: locked-scan boundary coverage -- direct child, locked DAT, nested tox/tdn/exclude, untagged nesting, tag-on-root -- non-file-backed DAT guards for
getExternalPath/setExternalPath, and theRemoveListerRowcompletion regression). Fresh-install smoke test of the shipped.toxin a throwaway instance: status Enabled, zero script errors, all extensions live, Envoy bound, clean packaged defaults, and both fixes verified behaviorally against the packaged build (nested tox/tdn/exclude locked ops suppressed with direct/plain-nested controls still caught; the exact field crash path -- a tracked path resolving to a selectDAT duringRefresh()-- completes and routes the row through the "replaced" recovery flow).
v6.0.123¶
Editable keyboard shortcuts (issue #50): every Embody binding is now remappable, recordable, and disableable from a new Shortcuts parameter page.
- Editable shortcuts (fixes #50): the seven combo shortcuts (Manager, Update All, Update Current COMP, Refresh, Export Project/COMP TDN, Copy TDN) are now Str parameters on a new Shortcuts page -- type a combo like
ctrl+shift+o(normalized and validated on entry) or leave one empty to disable it. The hardcodedelifchain inkeyboardin_callbacks.pyis replaced by a par-driven dispatch table built in the newshortcutsmodule DAT.ctrlandcmdare DISTINCT modifiers naming physical keys: macOS keyboards have both (matched exactly --ctrl+shift+oandcmd+shift+oare different bindings, andctrl+cmd+kis valid), PC keyboards have only Ctrl, so Mac-authoredcmd+...bindings fold to Ctrl there at match/display time (values never rewritten -- they round-trip between platforms intact). Factory defaults use the platform's primary modifier (Cmd on macOS, Ctrl elsewhere). A combo may drive exactly ONE action: pressing an already-assigned combo while recording pops an explanatory dialog (via the auto-respondable_messageBox, so tests and the save window never freeze) and re-arms with a fresh timeout; typed duplicates revert with a warning. The tagger double-tap menu lists PHYSICAL keys and adapts per platform via a livemenuSource: macOS offers Left Ctrl plus left/right Cmd (distinct keys, matched exactly; Apple keyboards have no right Ctrl); Windows/Linux offers left/right Ctrl (no Cmd key). A choice the other platform's keyboard lacks folds to its closest key at match time (Cmd->Ctrl on PC, right-Ctrl->left-Ctrl on Mac) -- the saved value is never rewritten, so it round-trips between platforms intact. - Shortcut recorder: each binding has a Record pulse -- press the keys you want; held modifiers preview in the status bar and the first non-modifier keydown commits the combo (the industry-standard rule: no premature commit while modifiers are held, no indefinite wait once a real key lands). Esc cancels; an armed recorder auto-disarms after 10 seconds. While armed, Embody's own dispatch is suppressed so pressing a currently-bound combo records instead of firing.
- TD built-in conflicts warn, never block: assigning a combo TouchDesigner itself owns logs a WARNING and shows it in the status bar (Embody cannot suppress TD's own shortcuts -- both fire). The TD reserved list is parsed live from the effective
TouchShortcuts.txt(factory table plus user overrides, honoring disabled rows) -- Embody cannot suppress TD built-ins, so the warning tells you both will fire. - Tagger double-tap is configurable: a menu picks which modifier key double-taps to open the tagger (left/right Ctrl, Alt, or Shift) or turns it off; requiring left-Shift specifically in the combo shortcuts is dropped (generic
shiftnow matches either side). - Bindings persist and surface everywhere: the shortcut pars (and the Enable Keyboard Shortcuts toggle, previously unpersisted) are in
_PERSISTED_PARAMS, so custom bindings survive Embody upgrades via.embody/config.json. Toolbar tooltips and the in-app help panel render the live bindings (tokens resolved at display time), and the six stale read-only shortcut display pars on the UI page are gone. Singleton detection now fingerprints onToxtaginstead of the removedAddtagshort. Newtest_shortcutssuite (48 tests: normalization, dispatch, reserved-list parsing, validation, the recorder state machine, parexec handlers, persistence whitelist). Test suite 92 suites / 2,080 tests.
v6.0.116¶
Two field-reported fixes -- removing a TDN externalization now sticks (issue #48), and Envoy no longer restart-loops on TD builds whose Textport stdout lacks isatty() -- plus version/minimum-build doc statements that rewrite themselves on save, a CONTRIBUTING guide, and five new specimen briefs.
- Removing a TDN externalization sticks (fixes #48):
RemoveTDNEntry(the manager's X button for TDN rows) deleted the tracking row and the.tdnfile but left thetdntag on the COMP -- and the Update sweep that runs on every save re-externalizes any tagged-but-untracked COMP, so the row and file the user just removed came back on the next save. Removal now strips the operator's externalization tags, resets its color, and drops its parameter-tracker entry (mirroringRemoveListerRow), and tolerates paths with no live operator (Full Project rows track/). NewTestRemoveTDNEntryregression class (5 tests), including a sweep-candidate check proving a removed COMP cannot be resurrected. - Envoy no longer restart-loops when
sys.stdoutlacksisatty(): TouchDesigner replacessys.stdoutwith a Textport catcher, and some builds (confirmed 2025.32460 on Windows, field report) ship one WITHOUT anisatty()method. uvicorn's default log formatter probessys.stdout.isatty()whenuse_colorsis unset, souvicorn.Config()itself raised ("Unable to configure formatter 'default'") before the socket ever bound -- and the liveness watchdog restarted the dead worker forever, freezing TD every ~10-25 seconds. Envoy now passesuse_colors=False(uvicorn's documented escape hatch; ANSI codes would be garbage in the Textport anyway). NewTestUvicornStdoutIsattyGuard(2 tests: the source pin, plusConfig()surviving an isatty-less stdout). A new troubleshooting section documents the symptom/fix, and the documented minimum build is corrected to 2025.32820 (builds that ship a catcher withisatty()). - Version and minimum-build statements now rewrite themselves on save:
execute_src_ctrl.updateVersionDocs(run byonProjectPreSave) rewrites the README version badge frompar.Version, the TouchDesigner badge year, and the minimum-build lines in README.md, docs/index.md, and CONTRIBUTING.md from the runningapp.build-- the build we save with IS the support floor, andapp.buildreplacesproject.saveBuild, which pre-save still reports the PREVIOUS save's build. Substitutions are anchored per line (changelog/history mentions of older builds are never touched), and each file is guarded so a missing doc can never abort a save. Newtest_version_syncsuite (6) fails on any drift between the badge, the three docs, andpar.Touchbuild. - CONTRIBUTING.md: a contributor guide for a repo where TouchDesigner writes many of the files -- contribution zones (open / TD-mediated / discuss-first), why TD-written files must not be reformatted, and how to run the test suite inside TD. Linked from the README.
- Five new specimen briefs (06-10): Bridget Riley's Current (analytic op-art GLSL), the Vasulkas' Rutt-Etra scan processor (TOP-to-geometry displacement), Vera Molnar's (Des)Ordres (seeded Python builder writing instancing tables), a Calder mobile (hierarchical transforms + CHOP physics-feel + shadow rig), and Ryoji Ikeda's datamatics (DATs as visual material on a frame-exact clock). The briefs README now covers the set of ten.
- Housekeeping: the
text_rule_refresh_after_committemplate DAT is converted from a mis-typed.pyto.md(it is a rule document, not Python), and the AGENTS.md template's rule 4 is realigned to the v6.0.111 deterministic-COMP-placement default (it still carried the old "current pane" guidance). - 91 suites / 2,032 tests (+13: issue-#48 removal regression, uvicorn isatty guard, version-doc sync). Full non-destructive run green (1,999 passed / 1 environment skip), and a fresh-install smoke test of the shipped
.toxin a throwaway instance verified: status Enabled, zero script errors, all four extensions live, Envoy bound, both fixes present in the packaged build, and clean packaged preference defaults (Filecleanup=keep,Toxdropexpr=ask,Envoyenable=0).
v6.0.113¶
TDN export survives broken widget clones (issue #46) and palette-clone blackboxing is restorability-gated -- verified end-to-end against the TauCeti preset manager (1,107 operators, 273 COMPs).
- Broken clone expressions no longer abort TDN export (fixes #46): truthiness on a
Parobject EVALUATES it, so_isPaletteClone'sif not clone_par:guard raisedtdErroron a widget clone expression referencing a missing master -- one line before the try written for exactly that -- and killed the whole export (including every subsequent checkpoint/save export of any tracked ancestor). Guards are nowis Nonewith the eval in its own try (_isPaletteClone,_getCloneSourceDiffs, plus the same landmine insetupBuildParameters); a raising clone expression exports as expression text, never evaluated. - Restorability gate for palette-clone blackboxing: blackboxing omits children/custom pars on the promise the clone master refills them on rebuild. That promise requires cloning ON and a master that resolves LIVE -- a disabled or unresolvable clone (TauCeti's widgets:
fadetimewith 7 authored children + 124 custom pars, cloning off, defensivehasattrexpression) was classified palette and would have rebuilt EMPTY. Classification (_isPaletteClone) and eligibility (_cloneRestorable) are now separate; unrestorable clones export in full. - Blackboxed palette clones actually restore on rebuild: export used to strip
clone/enablecloningfrom blackboxed entries, so a reconstructed shell had nothing to re-clone from and stayed permanently empty (a rebuilt lister: 0 of 31 children). The reference is now kept in the.tdnand applied on import BEFORE other parameter values (master content lands first, explicit exported values win -- the buttontype problem stays fixed), and only when the created op didn't auto-set its own resolving clone, so stale references in old files remain harmless. - Failed initial TDN export rolls the tag back:
applyTagToOperatorno-ops while the tag is present, so a failed export left a dead end -- tagged but untracked, every retry silently doing nothing until the user stripped the tag by hand (the "remove the tdn tag and press ctrlctrl again" complaint in #46). The tag now rolls back on failure with an ERROR log naming the cause; re-tagging retries. - Suffix-style custom parameter groups round-trip faithfully: export wrote the first COMPONENT's name for partial-arity groups (
Anchorxfor an XY group,Tintrfor RGB -- TD reports styleRGBAfor both RGB and RGBA,XYZWfor XY/XYZ/XYZW); import compensated by blindly stripping a trailing suffix letter, mangling legitimate base names (Labelbgcolor->Labelbgcolo+ r/g/b) and downgrading values-less RGBA groups to RGB (alpha silently dropped -- every TauCeti widget color par). Export now writes the group base name plus a true-aritysizefield (spec updated); import trusts the spec name (legacy component-named defs still detected narrowly) and picks the append variant from real arity. - Documented TD engine limit: extra children on an ENABLED clone cannot be restored programmatically -- TD wipes non-master children whenever cloning is re-established, regardless of ordering (verified three ways); only the native
.toe/.toxloader preserves that state. Such COMPs belong in TOX strategy ortdn_exclude. Captured where the importer handles clones and pinned by test. - Verified against the real component from the issue: export 1.2s (was: crash), full ctrl-ctrl tag flow end-to-end (tag -> table row -> 377KB
.tdn, all 14 broken clone expressions preserved as text),fadetimeround-trips 7/7 children + 124/124 custom pars. 90 suites / 2,019 tests (+14: broken-clone export and detection, restorable-blackbox round-trip, tag rollback with retry, RGBA/XY group fidelity).
v6.0.111¶
Deterministic COMP placement for AI agents -- build where the user already works instead of a different network each run -- plus a geometryCOMP default-torus trap documented at the point every agent hits it. Skills and templates only; no source or test change.
- Deterministic COMP placement via Embody-association. The
/create-operator"choose the parent network" step is rewritten so a new COMP lands in the SAME home every run instead of/one time and/project1the next. The default home is now the container that holds theEmbodyCOMP (op.Embody.parent().path) -- the level the user chose by placing Embody there -- with a deliberately-opened content pane (ui.panes.current.owner.path) as a guarded override that is IGNORED when it sits at the bare root/. Container names are still discovered withquery_network, never hardcoded to/project1. CLAUDE.md rules 3/5 (and the shippedtext_claude.md) were realigned to this default so the always-loaded north-star no longer contradicts the skill. - geometryCOMP: delete the default torus. A new
/create-operatorsection (promoted from/pop-networksso it fires for SOP and imported geometry too, not just POP builds) documents that a freshgeometryCOMPships with atorus1SOP whose RENDER flag is ON: the moment you add your own geometry, deletetorus1(or turn off its render flag), or the Render TOP draws BOTH your geometry and a phantom torus. The trap is easy to miss because adding your own SOP auto-clears the torus's exclusive DISPLAY flag (viewer looks clean) while its non-exclusive RENDER flag keeps drawing -- so it bites only livecreate_opbuilds (TDN import already strips these auto-defaults). - Docs: the never-before-released v6.0.109 features are now documented on the Envoy pages -- recovery hints in
docs/envoy/architecture.md+tools-reference.md, and thecapture_topQuality verdict intools-reference.md+index.md./pop-networksgains a cross-ref to the canonical create-operator torus rule. Suite unchanged at 90 suites / 2,005 tests.
v6.0.109¶
Two agent-ergonomics wins adapted from a competitor review: reactive recovery hints on failed tool calls, and a black/empty-frame verdict on capture_top.
- Recovery hints on error envelopes: when an Envoy tool returns an
error, arecovery_hintslist now rides back on the response -- each entry is{cause, action, next_tools}, matched by a small curated table (_recovery_hints_for) against the real error strings Envoy emits (path-not-found ->query_network/find_children, parameter-not-found ->get_op, wrong family, empty capture ->get_op_performance, thread conflict, timeout ->get_project_performance). Attached centrally in_send_responsevia_attachRecoveryHints: additive, never clobbers an existing block, never raises. It steers the agent's next step instead of a blind retry of the same failing call -- the reactive cousin of the.claudeskills. capture_topquality verdict: every capture now computes a token-lean verdict from the raw float pixels (_frame_quality) -- luminance + alpha stats yieldingis_black/is_flat/fully_transparent/pass/fail_reasons. It surfaces as aQuality: OK|FAILline in the returned text, so the agent can tell an empty render from a real one WITHOUT reading the image -- enforcing the "never declare a visual task done on a black frame" rule as machine-checkable data. A uniform fill is advisory (flat_frame), not a failure; black and fully-transparent are failures.- New
test_recovery_hintssuite (15): the match table against real Envoy error strings (including a liveget_opfailure) plus the additive/no-clobber/never-raise decorator behavior. 4 newtest_mcp_top_capturequality-verdict tests (black -> FAIL, noise -> OK, solid-colour -> flat-but-pass, transparent -> FAIL). Verified live from the release.toxin a throwaway instance: both features shipped in the packaged v6.0.109 build and work end-to-end (black capture ->Quality: FAIL ['black_frame'], bad path ->recovery_hints), extensions up, Envoy running, no script errors. 90 suites / 2,005 tests.
v6.0.108¶
A one-click Uninstall for removing Embody from a project -- guarded by a confirmation dialog that spells out exactly what will be removed before anything is touched.
- New Uninstall pulse (Embody page, right after Disable). It computes the same NON-DESTRUCTIVE plan as
PreviewUninstall(), then shows aui.messageBoxdescribing precisely what will happen -- how many Embody-generated items are removed (AI-assistant config likeCLAUDE.md/AGENTS.md/.claude/.cursor, Embody's.venv, the.embody/state folder), which shared files are modified by stripping only Embody's block/key (.gitignore,.gitattributes,.mcp.json), which git config keys are un-set (the.tdndiff driver), and which items are kept (files you edited, an unrecorded venv). It runs the teardown ONLY when you confirm; Cancel -- or a suppressed save/test context -- is a no-op, so nothing is ever removed silently. Your externalized.tox/.tdn/.pyfiles and the Embody COMP itself are never touched. - Wiring:
UninstallHandler(promoted) delegates toembody_admin.uninstall_handler, dispatched from theUninstallpulse viaparexec. The destructiveUninstall(confirm=True)API is unchanged underneath; the pulse just adds the interactive confirm gate on top. - Distinct from Disable: Disable removes externalization tags and stops tracking (re-enable with Update); Uninstall reverses Embody's install footprint on disk (config, venv, git wiring,
.embody/state). See the "Removing Embody" section in Getting Started. - New
test_uninstall_handlersuite (5): the cancel path removes nothing, a suppressed save/test context defers instead of uninstalling, an empty root reports nothing-to-do, confirm removes exactly the footprint, and an edited generated file survives via the review bucket. The confirm-path tests sealparexecsouninstall()toggling Envoyenable off never stops the live server. Atest_smoke_releaseassertion also confirms the release.toxships the Uninstall pulse + handler (verified live: a fresh v6.0.108 build loaded from the release.toxin a throwaway instance -- extensions up, Envoy running, no script errors, Uninstall param/handler present). 89 suites / 1,986 tests.
v6.0.106¶
The ext diet: EnvoyExt and EmbodyExt split into thin facades plus focused module DATs -- ~5,900 lines relocated with zero functional change, byte-identical MCP tool schemas, and three latent bugs fixed along the way.
- EnvoyExt: 9,221 -> 5,110 lines (-45%) across five module DATs:
envoy_layout(network lint + dock-hug + auto-position geometry),envoy_viz(the entire Embot + camera-follow subsystem, 29 methods),envoy_ops(19 mutating tool handlers),envoy_read(26 read/introspection handlers),envoy_setup(MCP/registry/git config, 21 methods). Every moved method keeps a delegating stub, so the public API, dispatch table, undo wrapping, and all monkeypatch seams are unchanged. - EmbodyExt: 10,217 -> 8,817 lines across three module DATs:
embody_launch(AI-client launcher),embody_git(AI-config/template/manifest generation + git status + InitEnvoy/InitGit/Reset),embody_admin(uninstall + settings persistence). The save/continuity/restoration engine core stays on the facade deliberately -- it is one interwoven subsystem, and splitting it would add risk without adding clarity. - Thread-safety rules enforced mechanically: worker-executed code (the docs lookup, the env/venv installer core, the git-status worker) stays on the facade because
mod.*is a TD object and off-limits off the main thread; the git-status worker now captures its parser as a plain module function resolved on the main thread. The env cluster's extraction was evaluated and correctly refused after thread classification. - Three real bugs fixed:
_checkMCPUpdatecalledrun()from its worker thread, so the MCP-update notice never logged (now an attribute-publish + bounded main-thread poll, with regression tests); a never-raises contract at the dispatch chokepoint was restored; the operator auto-position scan and overlap warning no longer treat annotations as obstacles. - Verification discipline: each package passed an adversarial review panel (AST-verified byte fidelity including every generated-template string literal, worker-closure audits, and a test-contract lens proving no suite went vacuous) plus live gates in the running TD session. Full suite green (the lone reported failure is the test-runner's own Status override, verified 33/33 via the direct runner).
- Module DATs ship in lockstep with the .toe so a fresh clone can never open a .toe whose extensions reference not-yet-restored module DATs (settings restore fires at frame 5; DAT restoration at frame 50).
- test_server_lifecycle grows to 24 tests (MCP-update marshal coverage). 88 suites / 1,979 tests.
- TDNExt's split (shared serialization/fileio/refs modules, then export/import/clipboard) is mapped and deferred to a follow-up -- see dev/embody/plan-ext-diet.md and the boundary map for the full extraction plan.
v6.0.104¶
Docked operators now hug their hosts mechanically -- dock placement moved from written guidance into the Envoy tool layer -- plus a docs transparency pass and the first Specimen brief set.
- Docked companions auto-hug their host.
create_opandcopy_opnow snap every docked companion an operator spawns (GLSL pixel/compute/info DATs, callback DATs) into a tight row 30 units below the host, centered, slots dock-width+20 apart (docks_placedin the result).set_op_positioncarries a host's docks along when you move it (docks_moved), so repositioning a GLSL TOP no longer strands its shader DATs -- the #1 way scattered docks actually happened. The auto-positioner also reserves the dock-row footprint, so a new host lands where its companions fit too. execute_pythonauto-fix + tighter lint. Docks of newly-created ops left scattered by a script are auto-hugged below their host before the layout lint runs (aLAYOUT WARNINGreports the fix); deliberate near-host placements are left alone. The scattered-dock lint threshold tightened from 500 to 350 units -- the old threshold let visibly-stranded docks (~400u away) pass silently.get_network_layoutreportsdockedTo. Docked companions carry their host's name, so the layout Verify step can check "every dock hugs its host" mechanically instead of by eyeball.- Skills/rules/templates synced: the create-operator skill gains an explicit docked-companions step and Verify item, network-layout.md documents the tool-layer enforcement (manual formula now scoped to
execute_pythonbuilds), mcp-tools-reference rows updated -- all three shipped templates regenerated and normalized to UTF-8/LF/no-BOM. - Docs transparency pass: honest build-time expectations (small changes in seconds, complete networks are 5-20 minute autonomous builds; multi-session parallelism as the real velocity story) across the landing page, manifesto, and quickstart; Auto-Externalize New Ops and Tool Permissions parameter documentation; Launch AI Client as the primary quickstart path; button-hover contrast and footer-spacing CSS fixes.
- Specimen briefs land in
dev/specimen-briefs/-- the authoring contract plus five briefs (point-line-plane, overture, digital-harmony, lumia, radiolaria) for the Specimen Collection gallery. .gitignorenow covers.embody/config dirs at any depth (machine-local catalogs/manifests no longer show as untracked).test_layout_lintgrows 11 -> 16 tests (hug formula, dock-follow, dock-self-move,execute_pythonauto-hug, 350 boundary). 88 suites / 1,977 tests.
v6.0.103¶
A "How should the AI ask permission?" step in the setup wizard so you choose your Claude Code tool-permission posture -- plus the wizard itself is now externalized to TDN.
- New wizard step: tool permissions. When you turn on Claude Code in the setup wizard (Auto or Advanced mode), a new step lets you choose how much Embody pre-approves Envoy MCP tool calls in
.claude/settings.local.json, so Claude Code stops asking on every tool use: Don't ask (recommended -- auto-approves all Envoy tools via themcp__envoywildcard, so new tools are covered too), Ask for some (read-only/query tools only; anything that creates/edits/deletes still prompts), Ask for all (pre-approve nothing), or Leave settings alone (never create or modify the file). The choice persists on a new Tool Permissions (Toolpermissions) parameter on the Envoy page. The step shows for Claude Code only, sincesettings.local.jsonis Claude-specific. - Captured TOPs no longer prompt to read. Every written posture also whitelists the operating-system temp directory (
tempfile.gettempdir(), macOS and Windows) inadditionalDirectories, so a PNG saved there bycapture_topcan be read back without a per-file permission prompt. - Non-destructive settings writer. The
settings.local.jsonwriter now merges into an existing file, preserving all your other keys (hooks, model, other allow patterns), only rewrites when the posture actually changes (no startup churn), and logs whether it created or updated the file. The shipped template shrank to a non-Envoy baseline; the Envoy allow entries are generated per posture in code. - Setup wizard externalized to TDN. The
wizardCOMP is now a first-class externalized artifact --wizard.tdn(structure) pluswizard/logic.pyandwizard/clicks.py(its hand-authored step machine and click router) -- diffable in git like Embody's other UI COMPs, and (as an Embody descendant) safely excluded from TDN reconstruction/stripping. - The permissions step fits the window. Its four options get a vertical scrollbar (with a peek of the fourth as a scroll cue) so the step never pushes the Back/Next footer off-screen.
- New
test_tool_permissionssuite (10) covers posture -> allow-list mapping,leave= no write, merge-preserves-keys, and idempotency;test_setup_wizardgains 3 for the posture plumbing. 88 suites / 1,972 tests.
v6.0.99¶
Setup-wizard layout polish and a size-aware network-spacing rule.
- Wizard option buttons: consistent vertical centering and left alignment. Every option button's title and subtitle now share one left edge and sit centered in the button on every screen (button text offsets normalized). Fixes the assistant/client/footprint buttons reading top-heavy while the mode buttons looked centered.
- "Review what Embody will add" hint fixed. The description text was
hmode='fill'-- spanning the full window with no right margin -- so long copy ran off the right edge and clipped, and it reserved a fixed 88px block that left a dead gap above the options. It is now constrained to the 452px content column (wraps cleanly inside the panel, no clipping) with a height that fits the wrapped text. - De-overlapped wizard button tiles in the network editor. The panel-widget button COMPs were stacked 100px apart while their tiles are 134px tall, so they overlapped in the editor (cosmetic -- panel layout is align-driven -- but messy). Tiles are now spaced by actual node height, zero overlap.
- New layout rule: spacing is
size + gap, both axes, never a fixed step.network-layout.md(and its shipped template) now specify computing every offset from actualnodeWidth/nodeHeight, with the vertical formulastep = ceil((maxNodeHeight + gap) / 200) * 200, and explicitly cover panel-COMP widget tiles (which the LAYOUT WARNING lint does not police). This is the rule that prevents the tile-overlap class of bug above.
v6.0.92¶
Setup-wizard text alignment (for real this time) and the last main-thread freeze removed from Envoy startup.
- Wizard option titles and subtitles are ink-aligned on every platform. Two stacked causes: the title used the multiline field renderer while the subtitle used the string label renderer (identical insets on macOS -- which is why it looked fixed there -- but divergent on Windows), and a size-14 first glyph carries ~2px more left bearing than a size-11 one. All 28 title/subtitle Text COMPs now share the multiline renderer (which also un-breaks subtitle word wrap, silently dead under string type), and subtitles carry a +2px offset compensation. Verified by pixel measurement: title and subtitle ink both start at column 19 on the Claude Code option.
- No more multi-second freeze after dependency install. The pip/venv install was already threaded, but the post-install import gate (mcp + pydantic + starlette + uvicorn, cold pyc compile) ran on TD's main thread -- field logs showed the frame counter pinned for ~6s. The gate now runs on a worker thread in both the first-install and every-open paths, with Envoy Status showing "Preparing Python environment..." during the warm-up and a once-per-TD-session flag so saves and server restarts skip it entirely. Verified live: server restart ran the new flow and recovered with the gate flag set.
- Setup-environment suite adapted and extended (thread-callable import gate, idempotent path wiring, session flag): 1,959 tests total.
v6.0.91¶
Rules diet: the always-loaded rule files shrink ~60 percent by relocating reference depth into four new on-demand skills -- every hard law stays inline, every moved section leaves a MUST-load trigger behind.
- Four new shipped skills (13 total):
/movie-export(the Realtime trap, zero-drop verification, deterministic export, async-reader staleness -- loaded only when actually rendering),/parameter-design(pages/styles/ranges/help-text catalog),/td-recovery(bridge internals + manual recovery runbooks),/multi-session-etiquette(advisory contract, claim leases, gates). - Thinned always-loaded rules: performance.md 21.0k -> 9.7k bytes, td-python.md 20.1k -> 15.2k, parameters.md 5.3k -> 1.5k, td-connectivity.md 6.1k -> 1.3k, multi-session.md 3.1k -> 0.9k -- roughly 10k tokens reclaimed per session, and per-project relevance: a project that never exports movies never loads the movie-export saga. Threading/cook depth moved into
/td-api-reference, which was already mandatory before writing TD Python, so enforcement is unchanged by construction. - Relocation verified mechanically: all 263 substantive lines from the original rules confirmed present in either the thinned rule or its destination skill (zero content lost).
- Peers hint: the FIRST multi-session advisory served to each session now carries a
_hintpointing at/multi-session-etiquette, so coordination guidance arrives exactly when a second session appears.
v6.0.90¶
Token and latency quick wins from the efficiency audit, plus the output-first visual convention.
- get_op on a diet. Returns NON-DEFAULT parameters only by default (include_defaults=True restores everything; parameters_omitted reports the filtered count). A parameter-heavy COMP read drops from ~13.7k chars to a few hundred -- the single largest per-call token sink, and it aligns with the non-default behavior users compared us against.
- Compact read shapes. query_network drops the redundant name field (derivable from path); get_network_layout drops name/family/node centers (centers = nodeX + nodeWidth/2, the math the layout rules already teach) and caps annotation text at 160 chars; get_parameter gains details=True with a lean default (keeps value/mode/expressions/menuNames).
- Docstring diet: -14.1k chars from tools/list. The 10 heaviest tool schemas trimmed from 20,902 to 6,754 chars -- eager-loading clients (Gemini CLI, Codex) stop paying ~3.5k tokens of teaching prose per session; the teaching lives in the mcp-tools-reference skill.
- ~5ms off every call. The worker's response delivery is now event-driven (blocking queue get) instead of a 10ms poll.
- Bridge tools/list no longer double-appends meta-tools on cache hits.
- Upgrade tracebacks silenced. The Envoy watchdog and TDN clipboard-watch reschedulers now null-guard their deferred callbacks, so replacing/upgrading the Embody COMP no longer prints AttributeError tracebacks from orphaned run() loops.
- Output-first visual convention (new). The visual-aesthetics skill, generated CLAUDE/AGENTS guidance, and dev docs now direct agents to create an Out TOP named out1 FIRST, turn on its display flag, and keep the working chain wired into it -- the user watches the piece take shape live in the network backdrop while the agent works.
- AGENTS.md parity. The non-Claude guidance gains the batch-operations rule and group-level (not per-op) verify cadence, matching CLAUDE.md.
v6.0.89¶
Launch AI Client fixes: Windows parity and visible errors.
- Windows terminal launches now guard for a missing CLI. Launching Claude Code / Codex / Gemini on Windows generated a raw
cmd /K "gemini"-- an uninstalled CLI produced cmd's cryptic "not recognized" error instead of guidance. Windows now gets a generated.battwin of the macOS.commandscript: awhereguard that prints the same install instructions and keeps the console open. Pure builder, unit-tested (goto-flow so hints with parentheses cannot corrupt cmd block parsing; CRLF). - "VS Code" launches again. The Aiclient menu and wizard offer VS Code, but the launch table only wired
copilotto the VS Code launcher -- selecting VS Code logged "No launcher" and did nothing on any platform. Added the missingvscodemapping. - Launch failures now show a dialog. Every failure path of the Launch AI Client pulse (no launcher wired, editor not installed, terminal failed, unexpected error) raises a message box with the install hint, in addition to the log line -- the message also now names the selected client instead of the parameter ("VS Code", not "AI Client"). The Windows missing-CLI case keeps its instructions in the opened terminal (no double dialog).
- Launcher suite grows to 29 tests (Windows batch builder content, vscode mapping, dialog consumption, label regression).
v6.0.88¶
Setup-wizard hotfix on top of the v6.0.87 feature release (below): the wizard's buttons were dead in every user project.
- Setup wizard clicks work in user projects again. The wizard's Panel Execute DAT watched 16 ABSOLUTE op paths (
/embody/Embody/wizard/...) that only resolve in the dev project; in a user project the tox lands at/Embody, the watcher resolved to nothing, and Next/Back silently did nothing (no errors -- the Auto option still looked selected because it is a native radio latch). Broken since the wizard shipped in v6.0.74; masked in dev and never exercised by the smoke harness, caught by the first real Windows click-through. Now uses relative patterns, verified end-to-end with simulated real clicks, and guarded by a new regression test that forbids absolute paths in any Embody panel watcher.
v6.0.87¶
Envoy grows to 53 MCP tools with undoable edits, official TD docs lookup, numeric TOP sampling, tighter parameter/script guardrails, explicit transport hardening, and new POP-building guidance.
- Undoable MCP mutations. Every mutating Envoy operation in
_UNDOABLE_OPSnow runs inside a TD undo block -- adapted from Derivative's TDMCP with permission -- so onebatch_operationsrequest collapses to one Ctrl+Z step instead of a pile of per-op edits. - Official TD docs from Envoy. New
get_docstool brings the live TD tool surface to 53 MCP tools and looks up official TouchDesigner docs by preferring the version-exact offline help mirror under<Samples>/Learn/offlineHelp, then falling back to docs.derivative.ca's MediaWiki API. Section drill-down usessections_available, and the HTML/API fetch work happens on the MCP worker thread so TD's frame loop stays clear. capture_top(sample_grid=...).capture_topcan now return a clamped 2..32 NxN RGBA sample grid instead of an image, with row 0 at the top and full-resolution per-channel min/max/mean. It is token-cheap, machine-assertable, preserves HDR values above 1.0 that image capture clips, and sanitizes NaN/Inf values.- Parameter search mode.
get_parameternow has glob search over parameter names, evaluated values, expressions, and bind expressions across a bounded subtree (search,search_in,depth,max_results), so searches likesearch="*/project1/*", search_in="expr"expose absolute-path expressions. - Safer parameter writes.
set_parameternow rejects invalid Menu values withmenuNames/menuLabelsinstead of accepting TD's silent index-0 coercion, includes a label-to-name hint when the caller sends a label, and auto-grows sequence-block parameters such asconst5nameto 6 blocks. execute_pythonrollback contract. A script exception now destroys operators created by that call and reports the rollback count, while mutations to pre-existing operators remain in place; the whole call is still Ctrl+Z-able, and the generated TD UI rule now states that true contract.- Transport security pinned. Envoy now passes explicit FastMCP
TransportSecuritySettingsfor Host/Origin validation and DNS-rebinding/CSRF defense instead of relying on SDK defaults; the security docs now say the localhost bind alone is not the defense. - New
pop-networksskill. The ninth shipped skill adds POP-family builder guidance adapted from Derivative's TDMCPSkillstd-pop-familywith permission: POPs vs SOPs, thegeometryCOMPritual, particle lifecycle,glslPOPdiscipline, trap list, and Embody's performance/layout/naming/verification gates. The template DAT,_TEMPLATE_MAP_SKILLS, release sync table, prerequisite row, and generated Claude list are wired. - New drift and tool-guard tests. Added
test_envoy_tool_guards(29 tests: undo wiring including live Ctrl+Z proof, menu/sequence guards, parameter search,execute_pythonrollback,get_docsparsing, andsample_grid) andtest_template_sync(5 tests: template map/disk/release-table sync plus orphan allowlist), bringing the source inventory to 87 test suites / 1,940 test methods. - Stale Envoy tests repaired. Six stale tests across
test_envoy_thread_comm,test_envoy_bridge, andtest_server_lifecyclenow match the current per-session log-cursor and_process_is_real_tdcontracts; queue-based thread-comm tests use private queues so they no longer inject fake requests into the live MCP queue or drain real sessions' pending calls. - Tool reference sync. The
mcp-tools-referenceskill/template, docs/envoy tools pages, docs index, and README counts are synced to 53 tools, includingget_docsandcapture_top'ssample_gridmode.
v6.0.83¶
Multi-session Envoy coordination, 52 MCP tools, and a TDN stability pass. This release makes parallel AI/client work more visible and safer, tightens destructive-operation behavior, hardens several TDN edge cases, refreshes the generated agent guidance, and regenerates the 6.0.83 release artifacts.
Envoy multi-session coordination¶
- 52 MCP tools. Envoy now exposes
claim_scopeandrelease_scopealongside the existingget_sessionsview, bringing the live TD tool surface to 52 tools plus the bridge meta-tools. - Live-session awareness.
get_sessionsnow reports recent scopes and claims so agents can see which peers are active and what part of the project they are working on. - Peer advisories on tool responses. MCP responses can include
_peersmetadata when another live session is active nearby, giving agents enough context to coordinate before editing the same network area. - Destructive-operation gates.
delete_op,import_network(clear_first=True),run_tests, andbatch_operationsnow refuse risky work when another recent session owns or touched the relevant scope unless the caller passesoverride=True. - Per-session log cursors. Recent log piggybacking is tracked per session, so one client no longer drains another client's warning/error feed.
TDN stability¶
- Import validates before clearing. Malformed TDN is rejected before
import_network(clear_first=True)clears an existing COMP. - DAT editability capture is non-mutating. Capturing
isEditableno longer changes the live DAT state while exporting. - Flag defaults round-trip more cleanly. Object COMP render/display defaults and noise terrain default flags no longer produce avoidable TDN churn.
- Stale cleanup is tracking-aware. Cleanup only removes tracked
.tdnfiles; ad-hoc untagged exports no longer enroll themselves into Embody tracking. - Orphan shell recovery remains intact.
_tdn_rel_pathrecovery for orphan shells is preserved across the export/import path. - Malformed templates degrade gracefully. Bad generated-template content is handled without cascading into broader TDN failure.
Setup and generated guidance¶
- Setup Wizard polish. The AI-client picker no longer forces a scrollbar now that the option list is shorter, and wizard copy has more right-side padding so text does not crowd the window edge.
- AI-client menu reflects current support. The standalone VS Code client token has been removed; GitHub Copilot remains supported through VS Code.
- Agent guidance updated. The generated multi-session rule/template and default MCP allowlist now include
claim_scopeandrelease_scope.
Tests and release artifacts¶
- New regression coverage. Added
test_tdn_stability_hardeningand expandedtest_envoy_sessionsfor multi-session coordination and destructive-operation behavior. - Current test source inventory. The repo now contains 85 test suites / 1,906 test methods.
- Release artifacts refreshed. The development
.toe, generated.tdnfiles, externalization table, and shipped release artifact were regenerated for 6.0.83.
v6.0.69¶
A new Dropped .tox Expression control, plus a data-safety hardening of the test harness driven by a real incident: destructive whole-project test suites can no longer run as part of a normal test run, so a full RunTests() can never mutate your live project.
Embody core¶
- Dropped .tox Expression (
Toxdropexpr). New menu on the Embody page controlling how the continuity sweep treats the default expression TouchDesigner auto-writes into a COMP's External .tox when a.toxis dragged in (me.parent().fileFolder + '/' + ...).Ask(default) prompts on detection with a list that truncates past a cap (so the dialog buttons stay reachable) and four choices -- Clean, Ignore, Always Clean, Always Ignore; the two "Always" buttons persist the choice into the parameter so you are not re-prompted. Embody's own descendants are always cleaned. The prompt now routes through the test-seedable_messageBoxinstead of a rawui.messageBox. - Removed self-heal param bloat.
_ensureAutosaveParams(EmbodyExt) and_ensureVizParams(EnvoyExt) recreated their own custom params on every init -- unnecessary, since params persist in the.toe. Both deleted; the params remain baked into the build. - Continuity sweep never touches Embody's own subtree.
checkOpsForContinuitynow hard-skips rows under Embody's own path, closing a gap where a transiently-missing Embody COMP could be deleted or re-externalized during strip/restore thrashing. - TDN
exportmode announces itself.ReconstructTDNCompsnow logs its export-mode action (additive recovery only, existing COMPs kept), matching theoffandfullbranches. - Fixed the shipped
CLAUDE.mdtemplate's accuracy.templates/text_claude.md(which generates a user project'sCLAUDE.md/ENVOY.md) still wrongly called.tox/.toe"text files" and described the deployed.claude/settings.local.jsonas read-only-only -- it had drifted behind the v6.0.66 accuracy sweep, so regeneration reverted that sweep. Corrected to match:.tox/.toeare opaque binary (.tdnis the text format), and Embody'ssettings.local.jsonpre-allows the write tools it actually deploys (create_op,set_parameter,execute_python,import_network, ...), written only if the file is missing and never overwritten.
Test-harness data safety¶
- Destructive whole-project suites are segregated. A test suite that calls
Disable/ExternalizeProject/Resetonext.root(the ENTIRE live project) now setsDESTRUCTIVE = Trueand is EXCLUDED from every normal run (RunTests/RunTestsSync/RunTestsDeferred*). Such suites run ONLY via the opt-in, save-gatedRunDestructiveTests(confirm_saved=True), which refuses on an unsaved project so a recoverable.toealways exists. A plain full run can no longer mutate the live project. New dev rulerules/destructive-tests.mddocuments the convention and the incident it prevents. Filecleanupcannot get stuck atdelete. The test runner's suppress/restore ofFilecleanupis now re-entrancy-guarded, so an interrupted or timed-out batch cannot leave it stuck atdelete-- a stuck value turns any file operation into a silent unlink.
Tests¶
- New
test_toxdrop_exprsuite (10 tests: the menu, all four dialog buttons, silent clean/ignore, Embody-descendant always-clean, and dialog-list truncation).test_dialog_suppressionhardened to diff the log buffer by entryidrather than a positional slice on a boundeddeque(maxlen=200). Test suite 76 suites / 1,761 tests; the normalRunTests()(1,729 tests, destructive suite excluded) is green with 1 conditional skip, and the 32-test destructivetest_custom_parametersruns separately viaRunDestructiveTests.
v6.0.66¶
A one-click Launch AI Client button on Embody's Envoy page: pick your assistant in the Aiclient menu, press the button, and Embody opens it at the project root -- editors (VS Code, Cursor, Windsurf; Copilot -> VS Code) open the folder as a workspace, terminal CLIs (Claude Code, Codex, Gemini) open in a new terminal. Built to survive the real cross-platform traps and hardened by a 10-agent codex cross-platform review.
Embody core¶
- Launch AI Client button (
Launchaiclient). New Pulse parameter besideAiclientthat opens the selected client at_findProjectRoot()(honoringAiprojectroot). One_AICLIENT_LAUNCHtable drives it; two helpers (_launchEditor,_launchTerminal) hold allsys.platformbranching. Editors resolve the REAL app/exe -- macOS via LaunchServices (/usr/bin/open -b <bundle-id>, then-a "<Name>", then the app's own bundled CLI), Windows via the realCode.exe/Cursor.exe/Windsurf.exefrom known install dirs -- never a hijackablecodePATH shim (Cursor installs its own). CLIs run in a real terminal so its login shell rebuilds PATH, which defeats the Dock-truncated-PATH problem where a CLI in~/.local/binis invisible to a Dock-launched TD (macOS writes a.embody/launch_<cli>.commandhanded toopen; Windows usescmd /K). A missing tool prints a verified per-tool install hint instead of a false "launched". - Fixes the "dock icon bounces, then closes" launch bug. TouchDesigner sets
ELECTRON_RUN_AS_NODE=1(plusLD_LIBRARY_PATH/DYLD_*/PYTHON*into its own bundle), and macOSopenforwards the caller's environment, so a freshly launched Electron editor (Cursor/VS Code/Windsurf) ran headless-as-Node and quit instantly. A new_launchEnv()strips those vars for every launch; verified live (Cursor stayed open). - Gemini config generation. Selecting Gemini writes a thin
GEMINI.mdthat imports the always-writtenAGENTS.mdvia Gemini's@AGENTS.mdsyntax -- no duplication. TheAiclientmenu gainscodex,gemini,vscode(the five existing tokens preserved verbatim so persisted settings never break). .gitignore: other AI clients' generated configs (.cursor/,.windsurf/,.github/copilot-instructions.md,.github/instructions/,GEMINI.md) are now ignored -- this repo's own client is Claude Code, whose.claude//CLAUDE.md/AGENTS.mdstay tracked.
Cross-platform review¶
- A 10-agent codex cross-platform panel (5 initial lenses + 3 verification, plus an orchestrator self-audit) drove the launcher to correctness: whole-body crash-safety in
LaunchAIClient; helpers returnboolso success logs only on a real launch; the Windows editor shim resolves viashutil.whichthen runs the.cmdthrough cmd's doubled-quote form (spaces +&/metachar safe);/usr/bin/openso launches work even if TD's PATH lacks/usr/bin;${SHELL:-/bin/zsh}used consistently in the generated.command. macOS is verified live; Windows is review-verified (bench-testing pending).
Tests¶
- New
test_launch_aiclientsuite (17 tests: launch-table shape, CLI resolution,.commandgeneration + quote-escaping, env sanitization, editor graceful failure) plus 7 newtest_claude_configtests (GeminiGEMINI.md+_clientFilesMissing). Test suite 75 suites / 1,751 tests, all green.
v6.0.62¶
A performance-rule expansion shipped in the .tox: a complete Movie Export / Offline Rendering playbook so an AI agent recording a movie never ships a juddered file. No core-code change -- this is agent guidance, delivered to user projects through the performance.md rule template baked into the build.
Agent rules¶
- Zero-dropped-frame movie export.
rules/performance.md(and its shipped templatetext_rule_performance.md) gains a "Movie Export / Offline Rendering" section built around the #1 cause of juddered exports: the Realtime flag (project.realTime, ON by default) silently replicating any frame TD can't cook within thecookRatebudget, so a recording ends up the right LENGTH but full of duplicate frames. The rule now mandates capturing the prior flag and going non-realtime before a render; routing every exit path (last frame written, a force-cook exception, a drop-abort, and user cancel) through one_finish(prior)helper that restores the flag and stops recording, since there is notry/finallyspanning the asyncrun(delayFrames=...)driver; monitoring the Movie File Out Info CHOPtotal_frames_droppedduring the render and aborting on the first drop instead of discovering it after minutes of GPU time; and proving the result with two separate checks -- length (total_frames_written,ffprobe -count_frames) and uniqueness (total_frames_dropped == 0plusffmpeg mpdecimate/framemd5duplicate detection), because a juddered file still passes the length check. Includes a deterministic per-frame export recipe (type='stopframemovie',addframe.pulse()stepped one frame perrun(delayFrames=1), force-cook-and-confirm before each pulse), a "let the encoder drain before verifying" caveat, and a correction thatperformLongOperationis not a documented API.
Tests¶
- Test suite unchanged at 74 suites / 1,727 tests -- this release is agent guidance shipped in the
.tox, with no Python code change.
v6.0.61¶
An Embot polish pass aimed squarely at the spawn-time frame drops, plus more character. The mascot now assembles off-view and swoops in whole instead of stuttering together in the net you're watching, and he picks up an occasional happy squint and a cleaner shrug.
Embody core¶
- Embot spawns without the frame-drop sag. Copying an annotateCOMP into the network you are viewing costs ~280ms (the in-viewport annotation-layer redraw); copying it outside the viewport costs ~100ms (measured). So on an on-screen spawn Embot now assembles at an off-view staging point just past the viewport edge and swoops in once whole -- each part's copy renders off-screen, so the fps sag is far shallower and you see a clean entrance instead of a stuttering build. Dives still snap in place (already cheap, off-screen). The fix was chased through
copyOPs,ui.pasteOPs, and a redraw-suppressed block copy -- all of which crash TD on repeat into a displayed net (one annotate at a time is the only stable primitive) -- before landing on off-view staging. - Paced, ordered assembly. The on-screen spread copies one part every
_VIZ_ASSEMBLE_INTERVALframes (32) in a body -> head -> speech -> limbs -> eyes order, so the per-part redraw hitches stay isolated instead of fusing into a freeze, and he reads as "building himself" rather than sitting half-built. - A happy squint. Every ~9-17s Embot briefly flattens and spreads his eyes into a content
^_^(separate from the ~2-5s blink). His eyes are a touch bigger now (12x13) so the squint has height to flatten from -- TD clamps an annotation node to a 10px floor, so the eyes must start tall enough to visibly squint (the same floor that made a scale-Y blink impossible). - Shrug, not stretch. The arms-up gesture used to lift the arms by scaling their height (a weird stretch); it now just raises them straight up.
Tests¶
- Test suite unchanged at 74 suites / 1,727 tests -- this is runtime character/camera behavior in
EnvoyExt, with no new Python unit coverage.
v6.0.57¶
A live-build-visualization split plus a major embody.tools Collection upgrade. In TouchDesigner, the opt-in build visualization (shipped in v6.0.54) is now two independent toggles -- the Embot character and the Envoy Follow camera -- and self-heals so it survives a restart. On the web, specimens gain multiple categories, private drafts, a license picker, and a meaningfully better TDN editor/profile.
Embody core¶
- The build visualization splits into Embot (character) + Envoy Follow (camera), each separately toggleable. v6.0.54 bundled the mascot and the camera under one
Envoyfollowswitch; they are nowEmbotenable(the little builder who stands on each operator and narrates what he just did) andEnvoyfollow(the network-editor camera that pans to the active op). The camera frames the operator now, so it follows Envoy's work whether or not the character is shown. - The toggles self-heal on every init. They were added live in a session and vanished on the next restart; a new
_ensureVizParams()recreates them if missing (idempotent, bakes into the.toeon save), so the feature is always controllable. - Per-frame bot assembly restored. Embot is copied from his template one part per frame -- the version that ran stably for hours -- replacing a single block
copyOPsthat was implicated in repeated TD crashes. - Past-tense narration. Embot describes the node he just finished and is standing on ("seeded a noise texture"), keyed on
OPType, with coverage expanded across TOP / CHOP / SOP / POP / MAT / COMP / DAT. - Follow no longer freezes on TD's auto-frame. The user-takeover detector now yields only on a real network change (you click into another COMP); a transient pan/zoom from TD auto-framing a freshly-spawned node used to stall the follow for ~6s while Embot raced off.
embody.tools¶
- Specimens can belong to several categories (up to 3). A new
specimen_categoriesjoin table backs ANY-match facet filtering and the category facet list;specimens.categorystays the primary (single-slot display + thumbnail motif + back-compat). Cards show a+Nbadge; the detail breadcrumb links each category. (D1 migration0010, backfilled.) - Private drafts + a publish toggle. New uploads default to private -- yours to preview and refine -- and you publish (or unpublish) from the specimen page or delete from your profile. Owner-scoped reads let you see your own drafts; everyone else sees only public. Your profile splits specimens into public/private groups with a persisted list/gallery view toggle and inline edit / arm-to-delete controls.
- License is a real picker. A fixed SPDX-style vocabulary (Creative Commons family + common code licenses + all-rights-reserved) replaces the free-text field on submit and edit; off-list values coerce to the default, and a legacy value survives an edit. The detail page shows the actual license.
- A better TDN editor + viewer. The editor gains a search match counter with prev/next/clear, a go-to-line popover, and paste-from-clipboard that unwraps an
_embody_tdnenvelope; the read-only viewer's jump menu now lists every top-level operator and annotation with type labels. Edit also accepts a replacement cover image (client-resized to 640x360). - Privacy: Inter is now self-hosted. The four woff2 faces ship from
/fontsand the Google Fonts CDN<link>is gone, so no visitor IP leaks to a third party. New cookie notice and copyright/DMCA pages round out the footer's policy set, alongside refreshed privacy and terms.
Tests¶
- Test suite unchanged at 74 suites / 1,727 tests -- the visualization split is runtime UI/camera behavior with no new Python unit coverage. The web added 2 Playwright e2e cases (multi-category submit + the 3-category cap).
v6.0.55¶
A clipboard UX fix. Copying a COMP's network with Ctrl+Shift+C no longer immediately prompts to paste it back into TouchDesigner -- an outbound copy (you are exporting it to share or paste elsewhere) is now distinguished from an inbound TDN (the web "embody it" button, a shared envelope).
Embody core¶
Ctrl+Shift+C(copy TDN) no longer turns around and offers to paste it back. The clipboard auto-paste watcher pollsui.clipboardand offers to "embody" any new TDN it sees as a new COMP -- but it could not tell your own outbound copy from an inbound one, so copying a COMP to share it fired an immediate "Embody it into ... as a new COMP?" prompt.CopyNetworkToClipboardnow seeds the watcher's last-seen signature with exactly what it wrote (re-read fromui.clipboardso it matches what the poll computes), so an outbound copy is recognized and skipped. An inbound TDN has different content -> a different signature -> still prompts, so paste-from-web is unaffected. Smoke-tested in the shipped.tox(a copy seeds the signature in the released build; clean boot, 0 errors). Newtest_outbound_copy_does_not_prompt+test_inbound_after_outbound_still_prompts.
Tests¶
- Test suite 74 suites / 1,727 tests, all green (+2 in
test_clipboard_watchfor the outbound/inbound distinction).
v6.0.54¶
A crash-resilience build. Embody now writes a cheap .tdn checkpoint of whatever changed after the agent (or you) goes idle -- so a TouchDesigner crash loses little unsaved work, with no full project save and no freeze. Plus an opt-in live build visualization (watch Claude build, with a little builder-bot), threading guidance that stops agents over-engineering data fetches, and a web contribute-form fix.
Embody core¶
- Auto-save crash checkpoints. A new always-on engine writes changed TDN COMPs to disk as a frame-cheap
.tdncheckpoint a beat after the agent or user goes idle -- no full project save, no TDN strip/restore, no frame freeze -- so an accidental crash (often agent-induced during a heavy build) loses little, and the checkpointed COMPs rebuild on next open. The key was measuring where a normal export spends its time: the dominant cost ofExportNetworkis therglobstale-file scan + cleanup (hundreds of ms), not the write (_safe_write_tdnis ~1.6 ms, serialization ~2 ms). A newskip_cleanup=Truepath onExportNetworkskips the rglob, the stale-file cleanup, and the modal size/lock warnings, so a single-COMP checkpoint lands at ~3-6 ms synchronous -- cheap enough to run inline on the main thread with no worker, no async, and no git churn from async-vs-sync output drift. - How it triggers. Mutating MCP ops record the touched TDN COMP (walking up to the nearest tracked boundary) and arm a ~1 s idle-settle timer; on settle, the touched COMPs are checkpointed one-per-frame. A destructive
delete_opof a child inside a tracked COMP also fires a synchronous pre-checkpoint before the delete, so a crash mid-delete still loses nothing since the last settle.execute_python/exec_op_methodare deliberately not checkpoint triggers (their effects are unbounded and opaque -- skip-and-document), andimport_networkis excluded from the pre-risky path because its.tdnis the user's source-of-truth being reloaded, not state to overwrite. - Recovery on open. In Export-on-Save mode (the default), reconstruction normally no-ops because the
.toeis the source of truth -- but a crash means the.toewas never saved, so any TDN COMP that is present on disk (.tdn+ a row inexternalizations.tsv) yet missing from the recovered.toeis rebuilt from its.tdn. This works becauseexternalizations.tsvis asyncfileDAT, so checkpoint rows reach disk within a frame without a project save. Recovery rebuilds nested TDN children with their own content (no empty shells), and a deleted COMP's tracking row is purged so recovery can't resurrect it. - Controls + self-heal. A new Auto-Save Checkpoints toggle (default ON) and a read-only Auto-Save Status readout (Idle / Saved / Bypassed / Disabled) appear on the Embody COMP's TDN page; both self-heal onto a fresh install of the shipped
.toxor an older.toethat predates them. The engine is bypassed in Perform Mode and during saves, and perf-gated so a checkpoint never piles onto a hot frame (it reschedules if FPS is under budget). - Verified by a 20-agent adversarial review (10 codex exec + 10 claude sub-agents) that caught and fixed real defects pre-merge: a pre-risky checkpoint over
import_network(clear_first=True)that would have overwritten the user's just-edited.tdn(data loss), nested-child recovery leaving an empty shell, a tracking-table mutation during the save window (crash), and an O(rows)-per-op lookup regression (now an O(1) keyed lookup). Newtest_autosavesuite (18 tests).
Envoy¶
- Live build visualization (opt-in). A new Envoy Follow toggle (default OFF, Envoy page) makes the network editor follow Envoy's work as Claude builds: within the viewed network it glides (ease-out) to center on each operator just touched; when the work moves to a COMP no pane is showing, it navigates a network-editor pane into that COMP and snaps to frame the op (you cannot glide across coordinate spaces). It yields the instant you pan, zoom, or navigate the view yourself and resumes once you stop. Main-thread only (driven from
_onRefresh) and side-effect-free with respect to saved files -- it writes only pane/view state, which is never externalized. - The builder-bot ("embot"). While following, a small figure built from minimal networkbox annotations (head, eyes, body, arms, legs) hops node-to-node along a parabolic arc, hovers when idle, and does occasional gestures (a wave, an arms-forward reach, an arms-up pump, and now and then a full robot dance). Its color reflects "thinking time" -- cool cyan-green right after Envoy acts, warming toward red the longer the gap between ops -- and the touched node pulses the Envoy accent. The bot and pulse retire after ~30 s of quiet and are destroyed before each save, so they never externalize.
Guidance¶
- Stop agents over-engineering threading for TD data fetches.
rules/td-python.mdand thetd-api-referenceskill (plus their shipped templates and askill-prerequisitescross-link) gained a "Background and Long-Running Work" decision ladder: reach for the Web Client DAT (async, main-threadonResponsecallback) or native JSON DAT -> DAT-to-CHOP chain for HTTP, the Palette Thread Manager only for genuinely blocking pure-Python work, and never a worker thread that touches a TD object or asleep/run()poller. The CLAUDE.md template gained the matching pointer, and the publishedtd-developmentthreading docs were updated to match.
embody.tools¶
- Contribute form gates submit until the required fields are filled.
Tests¶
- Test suite 74 suites / 1,725 tests, all green (
test_autosave, 18, added for the checkpoint engine).
v6.0.49¶
A generated-file-safety + web-polish build. Re-running Envoy's config generation (InitEnvoy, or flipping the AI Project Root) now PRESERVES your edits to generated rules/skills instead of clobbering them, via a content-hash drift manifest. The v6.0.47 annotation dedup reached Embody's own self-externalized .tdn files, and embody.tools got a deep TDN-viewer / Collection / YAML-viewer polish pass.
Embody core¶
- Generated files survive your edits (hash-detect).
_writeTemplaterecords a SHA-256 of each file it generates in.embody/generated-hashes.json. On regeneration (InitEnvoy, or flipping the AI Project Root) it now skips any generated rule/skill whose on-disk content no longer matches the recorded hash -- your edits win; delete the file to opt back into regeneration. Generated files stay byte-identical to their templates (sidecar manifest, no embedded hash); a legacy file with a marker but no tracked hash regenerates once, then becomes tracked and edit-protected. New tests B08-B12 intest_claude_config.
TDN format¶
- The annotation dedup reached Embody's own externalizations. v6.0.47 made the exporter capture annotateCOMPs only in the compact
annotations:array; saving the dev project re-exporteddev/embody.tdnanddev/embody/Embody.tdnthrough that path, dropping 9 redundantannotateCOMPoperator copies plus their now-deadtype_defaults/par_templates(840 lines removed) while keeping every annotation byte-identical in theannotations:section. Confirmed a safe dedup, not data loss, by a 20-agent adversarial review plus a field-by-field check (theannotations:block is identical before and after; 9/9 removed ops map 1:1 to a surviving native annotation).
embody.tools¶
- TDN network viewer. Correct TouchDesigner family colors (TOP purple, CHOP green, SOP blue, POP blue-violet, MAT olive-gold, DAT pinky-purple, COMP grey -- read from
ui.colors); op-reference parameters (a Feedback TOP's Target, etc.) now draw as dotted edges arcing over the tiles; node overlap is always prevented via a minimal nudge; a data wire that crosses an intervening same-row node arcs above it; the fullscreen control moved top-right and reveals on hover for card covers. - The Collection. A "by user" author filter (mirrors the category facet, SSR-applied, shown only when there is more than one author); the toolbar + grid are encased in one panel; the Collection nav stays highlighted on a specimen page; the breadcrumb category links to a filtered collection.
- Raw-TDN YAML viewer. Block-sequence keys (
operators:,annotations:) are now foldable -- their-items sit at the same indent as the key, so they were wrongly read as child-less; line-number gutter alignment fixed;+/-icons on expand/collapse-all; a show/hide toggle on the disclosure. - Specimen page + chrome. Badges + the "embody it" CTA moved to a sidebar to lift the network preview above the fold; equal-height columns and even section rhythm; the result thumbnail navigates to the specimen; a 3-10s page-load freeze fixed (the nav-glass html2canvas snapshot was rasterizing the whole YAML viewer); sitewide OG metadata + contribute-form polish.
Tests¶
- Test suite 73 suites / 1,707 tests, all green (B08-B12 added to
test_claude_configfor hash-detect).
v6.0.47¶
A TDN-format cleanup and save-UX build. Annotation COMPs are no longer double-captured in .tdn exports -- they lived both as a heavy operators: entry and in the compact annotations: array, dumping 100-205 lines of palette-clone boilerplate per annotation. The exporter now omits a null build number, the at-risk save check no longer mislabels a normal save as a test context, and all 12 affected gallery specimens were cleaned (-2,887 lines). Verified end-to-end: the shipped release .tox boots clean in a fresh-install smoke test with every fix live.
TDN format¶
- Annotation COMPs are captured ONLY in the
annotations:array, never asoperators:entries. A stock TD annotate is a palette clone with an extension and ~40 custom parameters; serializing it as a regular operator dumped well over 100 lines ofcustom_pars(everyOpviewer*/Body*) that exactly duplicated -- in a far heavier form -- what the compactannotations:entry already records: a single 205-line block for a single-annotate network, or a shared 183-linepar_templatesfor a multi-annotate one._exportChildrennow skips anyannotateCOMPchild (even a non-utility palette clone, which is how they leaked in); the importer already rebuilds annotations from theannotations:array (Phase 7a), so the op-list entry was pure dead weight. The TDN spec and JSON schema gained an "Export Behavior" note documenting the guarantee. build: nullis omitted entirely. Untracked / portable networks (no externalizations-table row, noBuildparameter) previously emittedbuild: nullin the header -- inconsistent with the format's omit-when-absent philosophy (position,size, etc.). Both export paths now drop the key when there's no build; older files carrying an explicitnullstill read fine.- 12 gallery specimens cleaned. Every affected
specimens/**anddev/specimen_lab/**.tdnwas stripped of its redundant annotate operators and the now-orphaned annotate-onlypar_templates/annotateCOMPtype_defaults-- 2,887 lines of pure deletion, validated by live re-import (every annotation rebuilds from theannotations:array with its title intact) and by clean reconstruction on project open.
Save UX¶
- The save-time "TDN Content at Risk" check no longer logs a misleading
[test]warning. During a project save,_messageBoxsaw the_suppress_dialogssave-window flag and mislabeled it as a test context, logging[test] No response seeded for "TDN Content at Risk" ...on every Ctrl+S whenever a TDN COMP held unprotected DAT content. The test gate (_smoke_test_responsesseeded OR a runner active) and the save gate (_suppress_dialogs) are now separated: a real test still warns loudly so test authors notice an unseeded dialog, while a save returns the safe default quietly (DEBUG) -- no more textport spam. Return values are unchanged.
Tests¶
- New
test_tdn_annotation_exportsuite (7 tests): annotate excluded fromoperators:, present inannotations:, no heavycustom_parsdump, round-trips via theannotations:array, andbuildomitted-not-null. Two newtest_dialog_suppressiontests guard the save-vs-test split (a save stays quiet; a real run still warns). Test suite 73 suites / 1,702 tests, all green; the shippedEmbody-v6.0.47.toxpassed a fresh-install smoke test (Embody/Envoy/TDN loaded, no script errors, both TDN fixes confirmed running live).
v6.0.46¶
A docs-accuracy and web-polish build. A multi-agent audit swept the entire docs site, the AI machine-files (llms.txt / for-ai), and the README against the live source and fixed every stale claim; the embody.tools web app gained an app-native report dialog, a simplified specimen preview header, a centred contribute form (renamed /submit -> /contribute), and a themed 404; plus minor custom-parameter organization on the Embody COMP.
Docs audit + reconciliation¶
- Community-paste model corrected. The platform docs (Collection / index / contribute) still described community specimens as pasting "inert by default" unconditionally -- stale since v6.0.44. They now describe the real verdict model: a clean specimen pastes live and fully working, a flagged one imports disarmed (provably-pure value expressions preserved), and a blocked one is rejected.
- Counts reconciled to ground truth. 72 test suites / 1,693 tests everywhere (testing.md's per-suite breakdown regenerated from real per-file counts); 49 MCP tools across README, machine-files, and landing pages (was 48 --
diff_tdnwas added after the count was last reconciled);for-ai.jsonversion bumped to current. - API + shortcut fixes. Wrong Manager shortcut (
Ctrl+Shift+Oopens it, notCtrl+Shift+E, which exports); non-existenttagOp()->applyTagToOperator();getExternalizedOps()->getExternalizedOps(COMP)(the method requires an op-family) -- corrected in the docs AND the shipped AGENTS template; the Claude Code rules/skills tables corrected to what Envoy actually generates (the 6 shipped rules,+/visual-aesthetics, no.claude/commands/); a broken#parametersanchor and a mistargeted Clipboard Auto-Paste link. llms-full.txtTDN spec regenerated from the current v2.0 specification (was a v1.3 snapshot -- missing the Back-compatibility section and the v1.4/1.5/2.0 changelog), ASCII-folded for the machine-file contract; the embedded MIME type corrected toapplication/yaml.
embody.tools web¶
- App-native report dialog replaces the browser
prompt()-- a themed<dialog>reason picker. - Specimen network-preview header simplified: dropped the "{name} graph" title and the "inert preview" badge, styled to match the rendered-result panel.
- Contribute page (renamed
/submit->/contribute): app-styled<select>dropdowns with proper arrow spacing, centred form column, consistent with the manifesto. - App-native 404 page replaces the default Astro 404.
Build¶
- Minor custom-parameter organization on the Embody COMP.
v6.0.44¶
Specimens from embody.tools now paste in LIVE and working, plus paste-placement and active-window fixes. The community safe-import was zeroing EVERY parameter expression -- a published specimen's GLSL uniform bindings, resolution, and animation drivers all collapsed to 0, so every pasted specimen rendered a dead frame. It now preserves provably-pure value expressions and disarms only genuinely side-effecting surfaces.
Community paste: specimens paste in working¶
- safe_import preserves pure value expressions.
make_inertno longer collapses every=expr/~bindto a constant. A new AST pure-value-expression allowlist (scanner.is_pure_value_expression) classifies an expression as safe iff it is provably side-effect-free -- par reads,absTime,math.*,Par.eval(), arithmetic, ternaries,hasattr-- andmake_inertneutralizes ONLY expressions that are not (any side-effecting call, dynamic-attribute / dunder / lambda / comprehension / f-string escape, import, mutator method). Verified against a 70-case corpus: 29 benign idioms preserved, 41 attack patterns neutralized, includingop('x').destroy(),__import__, walrus/lambda aliasing, and__globals__/mro escapes. - Scanner false positives fixed. The denylist scanner flagged the standard TD idioms
parent().par.X.eval(),.store(), andtdu.*as the Python builtins of the same name, and mis-scanned GLSL shader DATs as Python (a parse error counted as an execute surface). Parameter-expression danger now gates on the pure-value allowlist, and shader / data DATs (detected bylanguageor fileextension) are no longer AST-scanned as Python. The DoS bounds (AST depth / node-count / source-length) still block. - Live-if-scanned-clean routing.
CollectionExt.PlanCommunityPasteimports acleanspecimen LIVE (no neutralization), and aflaggedone inert-but-preserve-pure -- so a clean specimen pastes fully working with no warning. - TD palette extensions trusted, with hijack defense. An extension resolving through a TD built-in palette shortcut (
op.TD<Name>-- e.g. the standard Annotate COMP) is trusted (not disabled). Communityopshortcut(global op-shortcut) registration is stripped on import so a malicious TDN cannot hijack a palette shortcut (e.g. register its ownop.TDAnnotate) to repoint a trusted reference at attacker code; scopedparentshortcutis kept. - Adjacent surfaces closed. Script DAT/CHOP/TOP/SOPs are bypassed (they run Python on cook),
tox_ref/tdn_refshells are stripped, the untrusted import runs with the target COMP's cooking suspended (closing the param-set-before-bypass-flag window), andis_inertis purity-aware.
Paste UX¶
- Pasted COMP auto-selects and the view pans to centre it. It was landing off-screen / far-right. TD's network-view rectangle (
pane.bottomLeft/topRight) reports stale coordinates from a script andpane.home()/homeSelected()are no-ops unless the pane is focused, butpane.x/pane.y(the network coordinate at the pane centre) IS writable -- so the new COMP is placed beside the network, selected on its own + made current, and the view is panned onto it. - The auto-paste prompt fires only while the TD window is active. It was popping up while you were in the browser, and switching back left it stuck (the cursor-rollover signal only updates on a mouse-move). It now compares the OS frontmost-application PID to TD's own PID -- cross-platform (NSWorkspace on macOS, GetForegroundWindow on Windows, fail-open) -- and the latest clipboard wins when you return to TD.
Tests¶
New test_collection_pure (14, in-TD: the validator, preserve-pure neutralization, scanner verdict, GLSL/script/tox_ref/opshortcut handling, and live-if-clean routing) and standalone Collection/tests/test_safe_import_pure (25); test_clipboard_watch gains an active-window-gate test. Affected suites verified green: collection safe-import (18), scanner (22), collection-pure (14), clipboard paste (42) + watch (6), plus the standalone 70-case validator corpus. Test suite 72 suites / 1,693 tests.
v6.0.42¶
Clipboard auto-paste: bring a TDN into your network with no keyboard shortcut. Embody now watches the OS clipboard and, when a TDN network appears (copied from the web "embody it" button, or Cmd-Shift-C on a COMP in TD), prompts to "Embody it" into the current network as a new COMP.
- No-shortcut paste via a clipboard watcher. The old Cmd-Shift-V paste binding is gone -- TD's native operator-clipboard paste fires on the same keystroke and cannot be intercepted or suppressed, so it pasted leftover TD nodes alongside the TDN. In its place, a generation-guarded
run()-loop pollsui.clipboard(~1.5s) and, when a NEW_embody_tdnenvelope appears, offers (via the Embody message box) to Embody it into the current network. It is debounced (one prompt per copy; a dismissed envelope never re-nags), gated on a new Clipboard Auto-Paste toggle (default on), skipped in Perform Mode, and the prompt self-suppresses during saves and tests. Copy (Cmd-Shift-C) is unchanged. Newtest_clipboard_watch(5 tests);test_clipboard_paste(42) green, no regression. Test suite 71 suites / 1,678 tests.
v6.0.41¶
The git-uncommitted status axis: the manager gains a second status axis, completing the v5.0.437 feature set on the v6 line (after diff_tdn in 6.40). Externalized files saved to disk but not yet committed to git now show a distinct orange Strategy badge, kept separate from the red "unsaved" axis.
- Second status axis -- git-uncommitted. Externalized DAT scripts use TD's bidirectional syncfile, so they are always in sync with disk -- their only meaningful "changed" state is git-relative (on disk but not committed). A
git status --porcelainscan runs ASYNC on a worker thread (no refresh-frame drop;--no-optional-locksso it never contends with a concurrent commit), maps the changed files to operator paths via pure string math, and stores the result at runtime (never written toexternalizations.tsv, which would churn). The manager renders a distinct orangeUncommittedcolorbadge for TOX/TDN/DAT alike, overriding only the SAVED states (red unsaved + amber par-change keep precedence). Self-disables outside a git repo. The engine (_findGitRootSync,_parseGitPorcelain,_mapChangedToOps,_rowHasChanges,_updateGitStatus) is generation-guarded so a stale worker cannot clobber a newer scan. - A
changedfilter keyword + a refresh-after-commit rule. Typing "changed" in the manager filter shows only rows with pending changes on EITHER axis -- unsaved (dirty/Par) OR git-uncommitted -- via the single-source-of-truth_rowHasChanges. A shippedrefresh-after-commit.mdrule reminds agents to refresh the manager after a git commit so the orange badges clear. - Adapted to v6 + verified. The async scan uses
op.TDResources.ThreadManager; theUncommittedcolorparam (already present in v6 from a partial attempt) is now fully wired. Backend logic is covered bytest_git_status(20 tests), and the full data path was verified live (scan -> git_status storage -> lister git_state column -> orange badge). Test suite 70 suites / 1,673 tests, all green.
v6.0.40¶
The diff_tdn release: re-integrates the diff_tdn MCP tool and its companion .tdn git diff driver -- shipped on v5.0.437 but never present on the v6 line -- into v6's YAML v2.0 world, with a PyYAML-in-venv fix the YAML textconv needed and a 4-lens adversarial review that caught two real regressions before merge.
diff_tdn -- see what's unsaved, in one COMP or the whole project¶
diff_tdnMCP tool -- the UNSAVED view git can't give. It compares the live in-memory network against its on-disk.tdn, answering "what have I changed but not saved?" -- something git fundamentally cannot see (git only reads files on disk, never TD's live state).targetaccepts a COMP path or a.tdnfile path/bare filename (e.g."tooltip.tdn", resolved to its COMP) for one COMP in full per-field detail; omittargetfor a whole-project summary across every live TDN COMP (which changed + counts). The comparison is semantic, not byte-level: both sides normalize through the sametype_defaults/par_templatesexpansion the format uses, and the volatile export header (build,generator,td_build,exported_at,source_file) is ignored. Each change is{old, new}withold=disk,new=live, taggedkind: root | op | annotation. The engine lives inTDNExt(DiffLiveVsDisk/DiffAllLiveVsDisk+ the pure_diff_normalized);EnvoyExt._diff_tdnis a thin main-thread delegate.- Companion
.tdngit textconv driver for committed / history diffs. A rawgit diffof a.tdnis buried in export-header churn (a re-export bumps the timestamp/build even when nothing changed). Embody now installs a git textconv driver (.gitattributes*.tdn diff=tdn,.embody/tdn_textconv.py, andgit config diff.tdn.textconv, auto-configured on Envoy startup) that strips the volatile header before diffing -- sogit diff/git log -p/git showon a.tdnshow only real network changes, and a no-op re-export shows nothing.diff_tdncovers the unsaved window git can't see; the driver covers the committed view git owns. - Adapted to v6's YAML v2.0
.tdn, with a real PyYAML venv fix. Unlike main's JSON.tdnand pure-stdlib textconv, v6's.tdnare YAML, so the textconv is YAML-aware -- and git invokes it via Embody's venv python, which lacked PyYAML and silently fell back to a raw (noisy) diff.pyyamlis now a venv dependency and_environmentNeedsInstalldetects its absence to upgrade existing venvs. The diff engine also reconciles a legacy v1.5 array-of-linesdat_contentwith the v2.0 joined-string form so an unchanged DAT does not false-diff across the format bump. - Discoverability:
get_externalizations/get_externalization_statusrecommenddiff_tdn. Each externalization row now reports itsstrategy,absolute_path, and arecommended_tool: diff_tdnhint for TDN COMPs; the MCP tool reference documents when to reach fordiff_tdn(unsaved) versusgit diff(committed, kept clean by the driver). - Reviewed by a 4-lens adversarial panel that caught two real regressions pre-merge. The panel (spec-fidelity, correctness, TD-safety, integration) flagged a dropped
_get_externalizationsenrichment and a_environmentNeedsInstallchange that broke four existing setup-environment tests -- both fixed and verified. New suitestest_tdn_diff(11) andtest_tdn_diff_engine(25, including the dat_content reconciliation) cover the full handler chain and the pure engine. Test suite 69 suites / 1,653 tests, all green.
v6.0.39¶
The save-resilience release: a project.save() no longer freezes TouchDesigner with onboarding modals, and a long-standing watchdog bug that let the Envoy MCP server stay wedged after a save is fixed at the root -- the server now self-heals in about a second. Plus comprehensive v6 test coverage (169 new tests across 9 suites).
Envoy: the liveness watchdog now actually self-heals a save-time wedge¶
- Root cause: the watchdog's revive cooldown compared a per-launch frame counter against a value saved across launches.
_reviveDeadServermeasured its ~2s anti-spam cooldown inabsTime.frame(frames since the app launched -- resets to 0 every launch) but stored that value in COMP storage, which persists into the.toe/.tdn. A high frame value baked from a prior session made every revive compute a negativenow - storeddelta -- always "less than 2s ago" -- so the guard returned before scheduling the restart, every single time, permanently. The watchdog detected the wedge forever but was structurally forbidden from fixing it. The cooldown now usestime.monotonic()on an instance attribute (neverabsTime.frame, never storage);__init__scrubs the obsolete_last_revive_framekey. A fresh launch always starts un-wedged. A regression test stores a high frame and asserts the revive still fires. - The watchdog now trusts the socket, not internal flags. It keyed off
_init_completeand_starting, both of which aproject.save()resets -- so the tick went idle and never revived a genuinely dead server. It now keys off the visibleEnvoystatusplus a real socket probe: a dead socket while enabled revives regardless of those flags.Installing deps...is the one grace state it will not interrupt. Start()no longer trusts a stale "Running" status. It bailed if the status merely said "Running"; a worker that died without updating the status short-circuited the restart. It now probes the socket first and restarts on a dead one.
Envoy: the onboarding dialog never fires during a save or a test¶
project.save()used to surface the "Enable Envoy?" modal (sometimes many times), freezing TD. A single predicateEmbodyExt._suppressDialogs()-- true while a test run is active OR a save is in progress -- now gates the queue site inVerify(), the deferred_promptEnvoy, and_messageBoxitself, so the prompt can neither show nor queue mid-save.onProjectPreSavesets a_suppress_dialogsflag for the save window, scrubbed on next open so it never bakes a permanent suppression into the.toe. The file-cleanup and deprecated-externaltox prompts are gated the same way;_promptEnvoytreats a suppressed (-1) return as a no-op so a seeded test answer is still honored.
Tests: comprehensive v6 coverage¶
- 169 new tests across 9 suites (67 suites / 1,616 tests total): clipboard copy/paste (42), collection scanner (22) + safe-import (18), v6 hardening (20), specimen publish (19), the Envoy liveness watchdog (21), GLSL externalize (11), layout lint (10), and dialog suppression (6), plus
test_smoke_releaseadditions. - Layout lint
maxDepthfix. The v6.0.34execute_pythonlayout lint calledfindChildren(depth=12)(exactly depth 12 -- matched nothing); it now usesmaxDepth=12, so the lint actually fires.
v6.0.34¶
Everything since v6.0.26 in one release: a GLSL-shader externalization fix so shaders write as .glsl instead of .py, the recurring execute_python "(0,0) pileup" now caught by a layout lint at the Envoy tool layer, a self-contained Specimen publish hook for the embody.tools "embody it" copy-paste, and a waveform-stack feedback cook-loop fix — plus six landscape transmission specimens.
Externalization¶
- GLSL shader DATs now externalize as
.glsl, not.py.EmbodyExt._externalizeDATsinferred each DAT's externalization tag from a baredat_type_to_tagmap where['text'] = 'Pytag'— so every text DAT, GLSL shaders included (typetext, languageglsl), was written out as.py. It now resolves the tag from the DAT's content via_inferDATTagValue(which reads the text DAT's language/extension), so a shader externalizes with the correct.glslextension. This was the bug behind the content-safety "Externalize DATs" path mis-tagging shaders as Python. The 8 newer Specimens' 42 shaders were re-externalized to.glslto match the 4 older ones.
Envoy: layout lint at the tool layer¶
execute_pythonnow warns when it leaves operators at (0,0), overlapping, or with docked DATs scattered.create_opauto-positions;execute_python(rawcomp.create()/.copy()) did not — the recurring source of new operators piled at the origin. Envoy now snapshots the op tree before running your code and lints only the operators the call creates: a new_lintLayoutflags ops stacked at (0,0), overlapping op pairs, and docked DATs more than 500 units from their host, and_lintNewOpsemits aLAYOUT WARNINGon the response (via the notable-logs piggyback).network-layout.mdand its shipped template were DRY'd to state the trap once around the new enforcement and collapse the duplicate anti-pattern bullets.
embody.tools: Specimen publish¶
specimen_publish.py— a projectonProjectPostSavehook that exports each manifest Specimen self-contained (DAT scripts embedded) tospecimens/<tdn_path>, the form the embody.tools "embody it" copy-paste consumes. Unchanged files are skipped, so a save only rewrites the specimens that actually changed.
Specimens¶
- Waveform-stack feedback fix.
specimen_lab/waveform_stackhad a cook-dependency loop — the Feedback TOP's output wired back into its own input. Broken by seeding the Feedback TOP from outside the loop (res_fb) and grabbing the frame-delayed state from its Target TOP, the correct bounded-feedback pattern. - Six landscape transmission specimens added to
dev/specimen_lab(4KResw/Reshcontrol, shaders embedded): essence-streams, vertical-fibers, crosspoint (VHS glitch), waveform-stack (bounded feedback, up to 512 lanes), packet-fabric (GPU POP sim), and hyper_ntsc (NTSC chroma-bleed / dot-crawl); reaction-diffusion was landscaped with a bounded sim.
Test suite 58 suites / 1,439 tests, no regressions.
v6.0.26¶
A correctness + efficiency release that also finishes the TDN clipboard Copy/Paste loop: a critical TDN round-trip fix, the pre-save "TDN Content at Risk" dialog no longer firing on annotated specimens, the Envoy save-time watchdog log storm fixed for real, a four-part MCP token-efficiency pass, a fourth Specimen (a GPU flocking "Murmuration"), the Copy half of the clipboard wired to Cmd-Shift-C, raw-.tdn paste, and a POP point-sequence import fix.
TDN clipboard, paste & naming¶
Cmd-Shift-Ccopies the selected COMP to the clipboard. v6.0.11 shippedCmd-Shift-Vpaste and claimed a "Copy tdn button in the tagger" -- but that button never existed andCopyNetworkToClipboardhad zero callers, so a user had no way to copy. The copy half is now wired:CopySelectedToClipboard(Ctrl/Cmd-Shift-C) exports the COMP selected in the current network to an_embody_tdnenvelope on the OS clipboard. The loop is finally symmetric.Cmd-Shift-Vnow accepts a bare.tdndocument, not just an_embody_tdnenvelope -- so a.tdnfile's text copied from an editor pastes in. A bare.tdncarries no provenance, so it is sandboxed (scanned + default-inert) exactly like community content: a pasted stranger's.tdncannot run code. For a trusted local file,ImportNetworkFromFileimports it live. Parses YAML v2.0 and legacy JSON.- The clipboard envelope is pretty-printed.
to_clipboard_strswitched toindent=2, so a pasted envelope is human-readable. Thesha256is computed over the canonical innertdn(sorted keys, no spaces), never the clipboard string, so indentation changes nothing about integrity or web byte-parity. - A pasted COMP is named from the TDN's
network_pathbasename (e.g.,/specimen_lab/noise_terrain->noise_terrain), sanitized viatdu.validNamewith collisions uniquified -- no morepasted_tdn. No spec change:network_pathis required, so it already carries the name.
Fixes¶
- POP point-sequence
numBlocksimport fix. Pasting/importing a TDN with POP point sequences (e.g. aprimitivePOP/linePOPptsequence) loggedFailed to set numBlocks=N ... 'NoneType' object has no attribute 'numBlocks'and dropped the points. Cause:op.seq['name'](subscript) silently returnsNonefor POP sequences while iteration finds them (and attribute access raises). Export read them fine viapar.sequence, but import used the broken subscript. A new_getSequenceByNamehelper resolves sequences by iteration; all three import sites route through it. Regression test added; verified onnoise_terrain(point counts 2/5/6/8 restored exactly). -
test_tdn_file_io+test_tdn_helpersupdated for TDN v2.0 YAML. The v2.0 migration (v6.0.16) switched exports to YAML but left these 2 suites parsing withjson.load, so 33 tests had been red ever since (30JSONDecodeErrors + 3 assertion fails). They now parse withyaml.safe_load, and_read_existing_tdnrejects non-dict results (YAML parses garbage likenot valid json {{{into a scalar string where legacy JSON would have raised). Suites back to green (92/92 + 53/53). -
TDN custom-parameter VALUES now round-trip. Exporting a COMP with custom parameters and re-importing it silently reset every value to 0/min: the exporter omits a value that equals its default (intentional minimization), but the importer created the parameter with the right
.defaultand never set.val. So a default-valued custom par imported inert -- which broke every parametric specimen on import (noise-terrain's 10 params, murmuration's 10). Fixed with a default->value fallback in_setCustomParValues: when no explicit value is stored, single-component non-pulse pars initialize from their default; expression/bind values (which always carry a value) and multi-component defs are untouched. 6 new regression tests cover root + child COMPs, default + non-default, Float/Int/Toggle, and an expression-mode guard. - The save-time watchdog log storm is gone. A
project.save()reinitializes EnvoyExt many times in a rapid same-frame burst; each reinit armed a liveness-watchdog tick, and ~4s later they all came due in one frame and each revived + logged -- the "MCP socket on port None unreachable -- reviving server" line repeated 18-21x per save, plus an equal pile of redundant Start() schedules. The per-instance identity guard couldn't dedupe them because therun()reschedule string re-resolves to the current instance. Fixed two ways: a monotonic generation token collapses the leftover tick loops on the next reinit (so they don't accumulate over a session), and -- the real fix for the same-frame burst, where the generation counter doesn't accumulate -- a short frame-cooldown in_reviveDeadServercollapses all same-frame revives to one log + one revive. The genuine self-heal (revive after a save/reinit that left Envoy down) is preserved; verified on a real save (21 warnings -> 1). test_claude_configskills-count corrected (7 -> 8) --visual-aestheticshad been added to the shipped-skills map without updating the count assertions.- TDN content-safety scan ignores palette-clone internals. The pre-save "TDN Content at Risk" check walked into palette-clone COMPs and flagged their internal DATs/storage -- e.g. an annotateCOMP's button
helptables -- as user content at risk, so any TDN-tagged COMP containing annotations (every annotated Specimen) popped the dialog on every save. Both scans (_findAtRiskDATs,_findAtRiskStorage) now skip anything inside a palette clone: a clone's internals are regenerable palette boilerplate, never authored content. Verified live (11 at-risk -> 0).
MCP token efficiency¶
The Envoy MCP server was a large share of per-session token usage because tool results stay in context. Four changes cut response size dramatically:
_logspiggyback is now WARNING/ERROR-only. Every response previously glued up to 20 recent log entries on -- almost all routine INFO noise ("Processing:", the echoed code, "completed successfully") -- riding along on hundreds of calls per session. Now a_logsfield appears only when a WARNING/ERROR was logged during the call (capped ~8); the served-cursor still advances so nothing is re-served.get_logsremains the full-history escape hatch.run_testsreturns counts + failures only. The full suite's ~1,400 per-test PASS objects (~100k tokens in one response) are dropped; you get the totals and only the non-PASS results. Full per-test detail is in the test log file underdev/logs/.export_networkto a file returns a compact summary (op/annotation counts + file path), not the whole.tdnechoed back -- Read the file for details (which CLAUDE.md already prefers).capture_topno longer inlines the image by default. Base64 previews are token-heavy; it returns the saved file path (Read it to view). Passinline=truefor an embedded preview.
Specimen Collection¶
- Murmuration (4th specimen) -- a dense GPU particle swarm that flocks like a starling murmuration at dusk. True per-neighbor Reynolds flocking on the GPU: a Neighbor POP emits each point's neighbor index list, and a GLSL POP iterates those real neighbors for cohesion, alignment, and inverse-square separation (the key to even spacing -- a centroid-only force cancels inside a symmetric clump), plus a slow moving attractor, curl-noise wander, soft containment and drag. Rendered as additive point sprites with a speed-mapped dusk color ramp + bloom. Fully parametric (10 params), purely procedural, zero errors. The
specimen-authoringskill gained a GPU-particle/flocking section (POP feedback loops, neighbor-list GLSL iteration, point-sprite rendering, the startpulse transport, and the TDN param-value gotcha).
v6.0.16¶
The on-disk .tdn format graduates to TDN v2.0: YAML. Networks now serialize as a single self-contained YAML document instead of JSON, so a .tdn reads top-to-bottom like the network it describes — and your shaders and scripts read like code, not escaped strings.
TDN v2.0: the file format is now YAML¶
- YAML, a strict JSON superset. A
.tdnis now one YAML document. Multi-linedat_content(GLSL, Python, anytextDATscript) is stored as a plain string rendered as a YAML literal block scalar (|), so the source reads top-to-bottom with no escaped newlines and git diffs it line-by-line. This reverts the v1.5 array-of-lines workaround — the block scalar does the same job natively, and more readably. Short numeric vectors (position, size, color) stay inline ([200, -100]); longer or non-numeric sequences use block style. - Lossless and deterministic. Round-trips are byte-exact: trailing-newline count is preserved through automatic
|/|-/|+chomping, and the output is stable across re-dumps (no key reordering, no anchors) so re-saving an unchanged network produces no diff. Verified byte-identical on the shipped specimens including real shader text, tabs, and trailing newlines. - Reads legacy JSON — no migration gate. Existing
.tdn(versions1.x/1.5, written as tab-indented JSON) still import unchanged. Importers parse json-first: a document starting with{or[is read by the JSON parser (after stripping any leading UTF-8 BOM and whitespace), and only otherwise by YAML — so back-compat does not depend on a YAML C library, and tab-indented legacy files (which YAML forbids as indentation) load losslessly. Migration is lazy: a JSON.tdnis rewritten as YAML the next time Embody saves it. - Smaller files via boilerplate omission. Auto-created default docked compute DATs (the "Example Compute Shader" companion TD spawns alongside a
glslTOP/glslmultiTOP) are no longer serialized when unchanged — TD recreates the exact default on import. Combined with the YAML representation, files are roughly 17% smaller. The MIME type is nowapplication/yaml. - One-time migration diff. The first re-save of an existing JSON
.tdnproduces a one-time whole-file diff as it converts to YAML; this is expected and benign. A v2.0 YAML file cannot be read by a pre-2.0 Embody build (JSON-only) — new builds read old files, but not the reverse.
Docs¶
- The TDN Specification, format overview, examples, import/export, schema guide, and supported formats are rewritten for v2.0 YAML, with back-compatibility, literal-block
dat_content, chomping, and boilerplate-omission documented.tdn.schema.jsonvalidates the parsed structure, identical for YAML and JSON sources.
v6.0.11¶
Embody v6's Envoy + agent-guidance release: an MCP connection that self-heals across saves and reinits, the clipboard Copy/Paste loop with community-TDN safety, and new always-loaded guidance (crash avoidance + visual aesthetics) that deploys into user projects.
Envoy: the connection self-heals (the end of "connected:false")¶
- Liveness watchdog, tied to the EnvoyExt instance lifetime. The long-standing "connection dropped while TouchDesigner keeps running" symptom is fixed by a pure
run()-loop that probes the MCP socket every ~4s and revives Envoy whenever it is enabled-but-down — a dead socket, OR aproject.save()/ extension reinit that took the server down — force-freeing port 9870 if it is still held and rebinding in ~1s, with no restart and no manual toggle. It is armed from__init__(one loop per instance, dying only when a reinit replaces the instance, whose__init__arms a fresh one), so a save's mid-cycle reinit — which suppresses the old server thread's exit callback (no_scheduleRestart) and can skip or race the new instance's auto-start — can no longer orphan it. The earlierStart()-armed approach missed exactly this case. A stuck-_startingguard forces a revive if the startup poll loop dies; a tick error never kills the loop. Verified: killing the live listener self-heals in ~6s, and three consecutiveproject.save()cycles each had the watchdog fire (running=False), force-free the held port, and rebind in ~1s — the exact scenario that previously left the server permanently down.
Clipboard: Copy/Paste TDN, with community-source safety¶
- Copy/Paste networks through the TDN clipboard. Copy a COMP's network to the clipboard as a portable
_embody_tdnenvelope (the Copy tdn button in the tagger); paste it back with Ctrl+Shift+V as a new COMP. The clipboard pure-logic now lives INSIDE the Embody COMP (portable in the.tox), not a loose folder. - Community TDN defaults to inert. Your own TDN pastes apply directly (trusted); TDN whose source is
embody.tools(the community gallery) is run through a capability scanner and defaults to inert — Execute DATs disarmed, expressions neutralized, IO operators bypassed, storage stripped — while the content is preserved for inspection. Implemented as aCollectionExtextension plus self-containedscanner/safe_importDATs, with envelope hashing byte-compatible with the web contract.
New agent guidance (rules + skills), deployed to user projects¶
performance.md(new, always-loaded). Crash/freeze avoidance: a metric-gating protocol around heavy builds (baselineget_project_performance, re-check after each step, localize withget_op_performance), stop conditions with thresholds, a wiki-cited crash-cause table (resolution explosions, unbounded feedback, always-cooking operators, GLSL crashes, GPU/CPU exhaustion), and safe-default caps. Driven by wiki-verified TD performance research.visual-aesthetics(new skill). Objective composition / value / color / contrast / motion / finishing guidance — each as principle, TD technique, and failure mode — plus a mandatorycapture_toppreview-and-judge loop: never declare a visual task done on a black frame.- Preview-and-judge reinforced across
create-operator,debug-operator,mcp-tools-reference, andCLAUDE.md.td-connectivitynow documents the watchdog and "don't restart on a drop — let it self-heal."network-layoutis hardened against theexecute_python/.create()(0, 0) placement bypass. - All of the above deploy into user projects via the template map (
performance,visual-aesthetics, andtd-connectivitynewly registered).
This is the first changelog entry for the Embody/Envoy (TouchDesigner) side of v6; earlier v6.0.x builds were the embody.tools platform (web gallery, server-side scanner, backend) under platform/.
v5.0.429¶
A friendlier "Duplicate Path Detected" dialog: a naming convention that auto-resolves the common template-plus-copies case, a strategy prompt for oversized groups, and self-labeling buttons — so you can finally tell which operator is which.
Duplicate path resolution¶
- Feature:
Template Master Nameconvention auto-resolves duplicates. NewTemplatemasterparameter on the Embody COMP (default__template__). When a group of operators sharing one external path has exactly one whose path contains that name as a whole segment (e.g. a__template__parent COMP), it is auto-selected as the master and the rest are taggedclone— no dialog. This targets the common app-generated pattern of one template plus many runtime copies (e.g. ascene_<id>chain where each copy carries the template's externalized DATs). Opt-in by convention: projects that don't use the name see no change and still get the manual prompt; set the parameter to your own convention (e.g._master) or clear it to always choose by hand. Matches a whole path segment (not a substring), and only when exactly one operator matches — 0 or 2+ are ambiguous and fall through to the prompt. Persisted across upgrades viaconfig.json. Implemented as_resolveByTemplateMarker, wired intocheckForDuplicatesafter the clone/replicant resolvers. - The manual prompt no longer shows N identical buttons. Operators in a duplicate group usually share a name, so every selection button used to read the same (e.g. eight
fbx_callbacksbuttons with no way to tell them apart). Buttons are now labeled by the path segment that differs across the group, numbered to match the dialog body —1: __template__,2: scene_1exalohf, … (_duplicateButtonLabels). - Large groups get a strategy prompt instead of an unreadable button row. Above
_MAX_MANUAL_BUTTONS(5) operators, a button per operator overflows the dialog, so the prompt switches to Keep first as master / Dismiss and points at the Template Master Name convention for hands-off resolution next time (_promptForLargeDuplicateGroup).
Tests & docs¶
- Test: 1,413 tests (+12 in
test_duplicate_handling.py): convention resolution (single / zero / multiple / empty / custom-marker / exact-segment), the large-group threshold (at-threshold enumerated vs. above-threshold strategy and dismiss), and button-label disambiguation. - Fix:
test_envoyenable_reflects_server_stateskip-list. Its transitional-state skip set (Waiting/Starting/Stopping) predated the"Restarting after reinit..."status added by the v5.0.428 Envoy work, so it could fail during that settle window. AddedRestarting/reinitso it skips — rather than fails — that transitional state (note"Starting"is not a substring of"Restarting"). - Docs: new auto-resolution and button behavior documented in Duplicate Path Handling;
Template Master Nameparameter added to Configuration.
v5.0.428¶
Everything since v5.0.414, bundled into one release. The headline is tdn_exclude — a tag that makes a COMP invisible to the TDN system — alongside a rebuilt TDN dirty-detection pass that finally notices parameter edits without churning on live expressions, Envoy resilience hardening (honest startup status, a restart_td zombie fix, status relocated into the window header), a silenceable save-time content-safety dialog, three issue #21 crash fixes hardened across the whole table-read surface, a calmer first launch, and a final regression-review pass that corrected several rough edges before release. 57 test suites / 1,401 tests, all passing, plus a fresh-install smoke test of the release .tox.
TDN exclude tag¶
- Feature:
tdn_exclude— opt a COMP out of the TDN system. A newTdnexcludetagparameter on the Embody COMP (defaulttdn_exclude) defines a tag that makes a COMP invisible to TDN: never exported (notdn_ref/tox_ref, no structural reference), never stripped on save, never destroyed/recreated byReconstructTDNComps'sclear_firstimport. Primary use case: cascade-autotag bypass — whenTdncascadeis on, tagging a parenttdnpropagates to every child;tdn_excludeis the durable opt-out for app-managed children (spawned viaop.copy()at runtime, populated from user data — e.g. Moonshine'sproj_<id>chains). Runtime.copy()clones inherit the tag and stay invisible. Annotation COMPs are ineligible.getTagsfilters the exclude tag out of its selectors by parameter name, so naming it identically to a real tag never drops the real tag. Implemented acrossEmbodyExt(strip, dirty-detection, at-risk walks, cascade,_getTDNStrategyComps) andTDNExt(_hasExcludeTag, export,_collectAllPaths,clear_firstpreservation). Docs: Excluding a COMP from TDN. - Exclusion is honored at a TDN boundary's direct children; nested excluded COMPs are preserved, never lost. The strip/clear passes preserve an excluded COMP only when it's a direct child of the exported boundary. A COMP tagged for exclusion but nested under a non-excluded intermediate cannot be preserved by those passes — so rather than dropping it from the export while the strip destroys it (silent data loss), Embody now serializes it as ordinary content (it round-trips and survives) and warns that the tag had no effect at that depth, naming the COMP to tag instead. Export, fingerprint, strip, and
clear_firstimport all apply this rule consistently.
TDN dirty detection¶
- The dirty indicator notices parameter edits — without churning on live expressions. The per-COMP fingerprint now includes each operator's non-default parameters (its own custom pars and child operators'), recording the authored value —
exprfor expression mode,bindExprfor bind,valfor constant — neverpar.eval(). This matches exactly what an externalized.tox/.tdnserializes, so an authored edit flags dirty while a dependency-driven change to a live expression's evaluated value (a parameter bound toabsTime.frame, an audio level, a moving CHOP) does not — eliminating perpetual false-dirty re-export churn on animated COMPs. The same authored-capture rule governsParameterTracker.captureParameters(the TOX path), so both dirty mechanisms agree and neither has cook side effects. About-page metadata (Build/Date/Touchbuild) is excluded so build bumps don't dirty the COMP. Baselines are primed at the deterministic clean moments — right after externalize and after reconstruction. - One fingerprint sweep per Refresh. Dirty detection previously fingerprinted every TDN COMP twice per Refresh (an inline loop in
UpdateplusdirtyHandler) and re-scanned the externalizations table once per COMP per call — a visible frame hitch on large networks. It's now a single sweep indirtyHandler, withtdn_paths/exclude-tag computed once and reused, and the redundant per-COMPcompareParameterspass for TDN COMPs removed (the fingerprint already covers parameters). DirtyCountreads the fingerprint result for TDN COMPs. It previously used liveoper.dirty, which is alwaysTruefor a TDN COMP (emptyexternaltox), so every clean TDN COMP showed as dirty in the UI badge. It now trusts the table's fingerprint-deriveddirtyvalue for TDN-strategy COMPs (and still usesoper.dirtyfor TOX).- A reverted edit clears the dirty flag. The passive scan set
dirtywhen a COMP changed but never cleared it when the COMP became clean again, so the indicator stuck on after a revert. It now clears the flag when the fingerprint matches the baseline. - Fix:
_openFileLocationno longer logs a false warning on Windows —explorer /selectreturns exit code 1 even on success; switched tosubprocess.Popen.
Envoy resilience¶
- Envoy startup status tells the truth. Status no longer reads "Running on port N" optimistically before the server has bound. Start waits for a real readiness handshake (a worker-thread monitor sets
startup_eventfrom uvicorn'sstartedflag; a deadline-bounded main-thread poll flips status to "Running" only after a confirmed bind), and startup failures — including a uvicorn bind error raisingSystemExit/BaseException— route to the error path. A per-generation_startingguard prevents duplicate concurrent starts. - Envoy status moved into the window header, prefixed "Envoy". The standalone toolbar status widget is gone; live state now renders in the top header as "Envoy Running on port N" / "Envoy Disabled" / "Envoy Error: …", prefixed so it reads unambiguously. The stored status par value stays unprefixed, so EnvoyExt's status checks are unaffected.
- Fix:
restart_tdno longer false-matches zombie or foreign processes. Bridge process discovery now validates viaps(_process_is_real_td) that the process isn't a zombie and its executable basename is actuallyTouchDesigner, instead of a loosepgrep -f TouchDesignermatch.
Save-time content safety¶
- The "TDN Content at Risk" dialog can be silenced for good. When a save would drop DAT content or storage from a TDN COMP, the warning now offers a persistent "Always Skip" (sets
Tdndatsafety = 'ignore') alongside "Always Externalize" — both reversible via theTdndatsafetyparameter.
Externalizations table¶
externalizations.tsvno longer churns phantom timestamp rows per save.checkOpsForContinuitywas bumping thetimestampcolumn on every row each save (writing the externalized file's mtime, which the strip/restore cycle bumps for every.tdnregardless of content) — ~330 lines of diff noise per commit. The continuity scan no longer touches timestamps; the column now reflects only explicit Save/SaveTDN/rename events. Trade-off: an out-of-band edit (e.g.git pullbrings a new.tdn) won't auto-update the TSV timestamp — pulseRefreshto sync.
Crash safety (issue #21)¶
captureParametersno longer crashes on broken expressions. Reading authored values (expr/bindExpr/val) instead ofpar.eval()means a broken expression (ext.NotYetLoaded.X,op('./missing'), palette-clone expressions) can't raise during the dirty scan._cellValguards every externalizations-table read. TD returnsNonefor a missing column or a row-key miss, and.valon it raisedAttributeError— the issue #21 crash. The_cellVal(row, col, default='')helper was applied across the entireEmbodyExttable-iteration surface (not just the 5 sites the tracebacks pointed at), so the migration/continuity/dirty/dedup paths run against a partial or legacy table without crashing. It also logs a warning on a genuine row-level inconsistency (a short/partial row whose column exists in the header) so silent table corruption surfaces, while staying quiet on the normal not-found and legacy-missing-column cases.onProjectPreSaveno longer truncates the.toeto 0 bytes on an unhandled exception. The entire externalization pipeline (including the preamble) is wrapped in a fail-safetry/exceptthat logs and lets TD finish writing the.toe.
First-launch palette scan¶
- A fresh project on a new TD build no longer floods the textport with alarming (but harmless) errors. The shipped bootstrap catalog now covers build
099.2025.32820(projects on that build skip the live scan); the scan blocklist gained dependency-requiring families (tdAbletonPackage,ableton*,resources,world,system); and a first-launch banner frames any remaining scan errors as expected and one-time.
Window header / UI¶
- Fix: top-level manager rows show the expand/collapse glyph. Depth-0 rows with children now get one base indent level so the +/- affordance renders at the same offset depth-1 rows use.
- Fix: removed a duplicate UTF-8 BOM that an edit had introduced at the top of
WindowHeaderExt.py— a secondU+FEFFbefore the docstring could raiseSyntaxErrorwhen the extension reinitializes.
Docs¶
- New AI-first Quickstart page (
docs/quickstart.md) — install → drag in → Enable Envoy → connect your AI client, with per-client steps and troubleshooting; linked from Home, the nav, and the web landing page. - New "Excluding a COMP from TDN" section documenting the exclude tag.
- Reconciled the Envoy MCP tool count to 48 across all current-facing surfaces (the 4 bridge meta-tools are counted separately).
- POP skill corrections — POP = "Point Operators"; File In POP (meshes) and Point File In POP (point clouds) are distinct operators. Template twins synced.
- Rewrote the landing-page meta descriptions to experience-first copy.
- Fix: de-mapped dev-only
.claudefiles no longer self-delete on an AI-Project-Root flip (release-commits.md,multi-instance/SKILL.md) — markers stripped so cleanup treats them as hand-maintained dev files.
Tests & review¶
- 57 test suites / 1,401 tests, all passing. New and updated coverage across the changeset: 21 tests for
tdn_exclude(including nested-under-normal now preserved rather than lost), the TDN fingerprint/dirty-detection suite (the no-churn-on-live-expressions guarantee,DirtyCountstrategy-awareness, and clean-clear-on-revert), the widened issue #21 cell-read surface, the Envoy startup-status contract, and the save-time content-safety dialog. - Final regression-review pass before release. A 7-angle review of the branch (line-by-line, removed-behavior, cross-file, plus reuse/simplification/efficiency/altitude) surfaced and fixed: the live-expression dirty churn, the nested-exclude data-loss path, the always-dirty
DirtyCountfor TDN COMPs, the stuck dirty flag, the duplicate BOM, and the double fingerprint sweep. Each fix carries a regression test. - Fresh-install smoke test. The release
.toxwas loaded into a blank project in a separate TD instance and verified: statusEnabled, no script errors, all three extensions loaded, Envoy bound, externalizations schema intact, and the header status prefix present.
v5.0.414¶
Third value Custom for AI Project Root (follow-up to Ten0's feedback on issue #19) — lets the user pick any directory as the AI/MCP config root, not just git root or .toe folder. Useful for monorepos where multiple .toe files share a parent directory and should converge on one set of AGENTS.md / .claude/ / .mcp.json / .embody/ instead of duplicating per project. Plus two defense fixes against a previously-unobserved class of test interference: tests with exhausted or missing seeded responses no longer open real modal dialogs that freeze TD; Verify() can no longer queue multiple Envoy opt-in prompts in quick succession.
- Feature:
AI Project Root = Custom— new menu option on the Envoy page, paired with a newAI Project Root (Custom)Folder parameter. The custom path can be absolute (e.g./Users/foo/touchdesigner/) or relative to the.toedirectory (e.g.../for "one level up"). The Folder parameter is greyed out (enable=False) unless the menu is set toCustom. Flipping the menu, or changing the custom path while in Custom mode, migrates Embody state and AI config to the new location — same atomic move + marker-aware cleanup as the gitroot↔projectfolder flip. For Ten0's monorepo use case (#19 follow-up), each.toein the same parent dir just sets the same relative path and they all shareAGENTS.md/.mcp.json/.embody/envoy.json— which lets the multi-instance MCP feature work naturally across sibling projects. - Fix:
_findSettingsFilewalk-up fallback handles the Custom mode chicken-and-egg. At TD launch,Aiprojectrootsits at its baked-in default before settings are restored. For gitroot↔projectfolder, the alternate root is computable without readingconfig.json(just walk to.gitor useproject.folder). For Custom, the alternate path lives insideconfig.json— chicken-and-egg. Solution: after checking the predefined alternates, walk up fromproject.folderlooking for any.embody/config.jsondirectory. The user's saved custom location is found regardless of what the baked-in default says, so settings restore survives across restarts even when the user closed TD without saving the.toeafter a flip. - Fix: tests can no longer open real modal dialogs that freeze TD.
_messageBoxpreviously fell back toui.messageBox(...)when the test framework's seeded responses were exhausted or missing — a single-int seeded response is consumed on first use, so a test that triggered N dialogs got one auto-answer and N-1 real modal dialogs stacking up after the test finished, freezing TD with no way out short of force-quit. New behavior: when_smoke_test_responsesis set in storage (test mode), missing or exhausted responses return-1and log a WARNING instead of opening a modal. Belt-and-suspenders with the next fix. - Fix:
Verify()no longer re-queues the Envoy opt-in prompt while one is already pending. Tests that ran multipleVerify()cycles in succession (e.g.test_custom_parameters's Disable/Enable suite) would each hit theelsebranch and set_pending_envoy_prompt = True, stacking N prompts even with only one auto-response seeded. Gating ongetattr(self, '_pending_envoy_prompt', False)makes the flag idempotent — at most one prompt queued at a time, regardless of how many timesVerify()runs.
v5.0.413¶
Two independent bodies of work bundled into one build. First: issue #20 fix — parent .tdn files no longer embed the contents of TOX-externalized child COMPs (mirrors the existing tdn_ref pattern with a new tox_ref; TDN format bumped to v1.4). Plus the round-trip restore path, backward-compat strip for pre-v1.4 files, and a substantial Envoy-toggle frame-drop fix surfaced while diagnosing the broader change. Second: a new AI Project Root parameter for monorepo TouchDesigner projects (and the underlying fix for issue #19 — Path.home() length comparison broke on Windows non-home drives), with a cluster of safety improvements found by a 10-agent cross-AI review of the change.
TDN tox_ref and Envoy toggle perf¶
- Fix: issue #20 — parent
.tdnno longer duplicates TOX-externalized child contents. When a parent COMP was exported as TDN and one of its children was externalized via the TOX strategy, the parent's.tdnsnapshot recursed into the child and re-emitted the child's full subtree — including grandchildren types that then polluted the parent'stype_defaults(e.g.containerCOMP,outCHOP,parameterCHOPfrom sliders bloating the parent file, defeating the entire point of TOX externalization). The exporter already handled this for nested TDN children (viatdn_refsince v1.2), but the symmetrical TOX case was missing — the asymmetric behavior had no design rationale, just an unwritten gap. New_hasTOXTag/_resolveTOXRef/_getTOXExternalizedPathshelpers inTDNExtmirror the existing TDN trio;_exportSingleOpwrites atox_refpointer for TOX-tagged children and skips recursion;_collectAllPaths(async export) skips their subtrees as well. Both the metadata branch and the children-recursion branch were moved outside therecurse=Truegate so async modular exports also emittdn_ref/tox_ref(previously a latent bug — async produced shell-only children with no pointer at all). The TDN format version bumped to1.4and the schema (docs/tdn.schema.json) gained atox_refproperty; existing v1.3-and-earlier files continue to work via the externalizations table. - Fix: round-trip restore for
tox_refchildren._createOpsonly recognizedtdn_reffor shell-only COMP creation — atox_refentry would create an empty COMP with noexternaltoxparameter (externaltoxis inSKIP_PARAMS, so the exporter strips it). Combined withReconstructTDNComps'sclear_first=Truedestroying any TOX child thatRestoreTOXCompshad just rebuilt at frame 45, the round-trip was broken:.toxcontent was loaded, then immediately wiped, with nothing to refill it until the next project open. New Phase 8.5_restoreTOXShellswalks the imported tree, finds any shell carrying a_pending_tox_restorestorage marker (set by_createOps), setsexternaltoxfrom that marker, and calls_reloadToxto force TD to re-read the.tox. Runtime imports (e.g.import_networkvia MCP) and project-open reconstruction both now restore the.toxcontent immediately, with no second-save dance required. - Fix: pre-v1.4
.tdnfiles with embedded TOX children import cleanly. New_stripNestedTOXChildrenmirrors_stripNestedTDNChildren— consulted on import to empty outchildrenarrays for any path matching a TOX entry in the externalizations table. Otherwise pre-fix files would re-create the embedded grandchildren into the live network, thenRestoreTOXCompswould clobber them with the actual.toxcontent (or worse, the embedded shells would sit there if the table was somehow out of sync). The TDN-side strip already hadtdn_paths.discard(target_path)to avoid stripping the COMP currently being imported; added the symmetrictox_paths.discard(target_path)to the new strip. - Fix: cross-validation parity for
tox_ref. Added_validateTOXRefs(mirror of_validateTDNRefs) that warns when atox_refpoints at a path missing from the externalizations table or at a.toxfile missing on disk. Both validators run during every import. - Fix: Envoy toggle no longer drops ~108 frames per cycle. Diagnosed via temporary
ENVOY-PERF-STARTinstrumentation while looking at the broader fix._findAvailablePortwas payingtime.sleep(0.1) × 15 = 1.5son the main thread whenever a preferred port (e.g. 9870) was held by any listener, including foreign zombie TD processes that aren't in our.embody/envoy.jsonregistry — force-closing our own shutdown events and uvicorn handle does nothing for a foreign process, so the wait was guaranteed to expire pointlessly. Three layered changes: (a)_forceCloseOldServernow returnsboolindicating whether it actually closed a live uvicorn server of ours (sys._envoy_uvi_serverwas set) — re-signaling stale shutdown events for already-exited threads is housekeeping, not a port-holding signal; (b) the server thread'sfinallyblock clearssys._envoy_uvi_server(guarded by anisidentity check so a newer Start that already replaced the handle doesn't get clobbered) so a subsequent Start correctly sees "nothing of ours is holding the port"; (c)_findAvailablePortnow branches: if_port_registered_by_other(base_port)is True (foreign live instance in our registry), jump straight to scanning the range with no wait; if force-close had nothing to close (foreign zombie / clean prior shutdown), also skip the wait; only wait when we genuinely have a stale uvicorn handle of ours to drain — and even then, capped at 500ms (5×100ms) instead of 1500ms (15×100ms). Measured impact on the user-toggle path: total Start time dropped from ~1797ms → ~346ms,findAvailablePortfrom ~1527ms → ~10ms. - Fix: doc/code drift in TDN spec. Spec previously claimed the importer creates the TOX shell "with
externaltoxpre-set" — butexternaltoxis inSKIP_PARAMS(excluded from TDN export) and_createOpshad notox_refhandler, so the documented behavior was fictional. Now both the code does what the docs claim (via the new Phase 8.5) and the docs accurately describe the_pending_tox_restorestorage marker mechanism. Also added a "if a COMP carries both TDN and TOX tags, TDN wins" note indocs/embody/externalization.mdto document the previously-undocumented precedence (TDN'selifbranch in_exportSingleOpruns first). - Tests: 6 new in
test_tdn_file_io(test count 66 → 92):test_tox_ref_written_on_export,test_tox_ref_absent_without_tag,test_tox_ref_absent_with_embed_all,test_tox_children_stripped_on_import,test_tox_type_defaults_not_polluted(direct regression test for the issue #20 symptom — two sibling TOX children with identical internal structure no longer leak their grandchild types into the parent'stype_defaults),test_tox_ref_consumed_on_import(verifies_createOpsskips child creation fortox_refentries).
AI Project Root and issue #19¶
- Feature:
AI Project Rootmenu parameter (gitrootdefault /projectfolder) on the Envoy page. Controls where Embody writes AI/MCP config (AGENTS.md,CLAUDE.md,.claude/,.cursor/,.mcp.json,.embody/).gitrootpreserves prior behavior — config lives at the top of the git repo, which is what every AI tool expects when the whole repo is the workspace.projectfolderwrites config next to the.toeinstead — the right choice when your TouchDesigner project lives in a subdirectory of a larger repo and you open that subdirectory as your AI tool's workspace (e.g.myrepo/touchdesigner/opened as the Cursor or Claude Code root). Flipping the parameter migrates Embody's own state (.embody/config.json,project.json, palette catalogs,.claude/settings.local.json) to the new root and cleans up Embody-generated AI files at the old root. User-authored files (custom skills, hand-editedCLAUDE.md, other entries in.mcp.json) are preserved. - Fix: issue #19 —
Path.home()comparison no longer bails before searching on non-home drives._findProjectRoot(EmbodyExt),_findGitRoot, and_checkOrInitGitRepo(EnvoyExt) all comparedlen(parent_dir.parts) <= len(home_dir.parts)without checking whether home was actually an ancestor of the project. On Windows with a project onD:\and home onC:\, both paths have the same part count so the guard triggered immediately and the.gitwalk-up never started. Subsequent runs after the first successful git pick failed to find the repo, duplicated.mcp.jsonconfig, and broke the MCP connection. The fix only applies the home-dir stop when home is genuinely an ancestor (covers the original intent: avoid finding a stray.gitin~/.dotfiles) and falls through cleanly whenPath.home()raises. - Fix: Envoy registry I/O now honors
AI Project Root._writeEnvoyConfigalready wrote.embody/envoy.jsonunder_findProjectRoot(), but_port_registered_by_other,RefreshRegistry, and_removeFromRegistrystill derived the path from cached_git_root. Underprojectfoldermode, port-conflict detection would silently disable, refresh would write to the wrong file, and shutdown would leave stale entries in the registry. NewEnvoyExt._registryPath()helper routes all three readers through_findProjectRoot()(defensive fallback to_git_rootif the Embody extension isn't accessible). - Fix: cross-filesystem migrations are now atomic. Plain
shutil.movefalls back to copy + delete across filesystems — a crash mid-copy leaves a partial destination while the source is gone. Palette catalog files (large, expensive to regenerate, not rebuilt from settings) were the worst exposure. New_atomicMovehelper copies to a sibling tmp file,os.replaces atomically into place (single-filesystem rename), then unlinks the source. A failed copy never leaves a half-written destination. - Fix: settings restore now survives the
AI Project Rootchicken-and-egg. On TD launch,init()doesn't touchAiprojectroot, so the parameter sits at its baked-in default (usuallygitroot) when_restoreSettingsruns. If the user previously flipped toprojectfolder, the savedconfig.jsonlives at the project folder, not git root — the canonical_settingsPath()would miss it and silently bail, reverting every persisted setting (including theAiprojectrootvalue itself) on every restart. New_findSettingsFile()checks both candidate roots before declaring the file absent and logs which one it picked up. - Fix: half-migration orphans are quarantined. If
_atomicMovefails forconfig.jsonorproject.jsonmid-flip, the source remains at the old root. The post-Pass-1 sweep renames any leftover critical file to.json.orphanso_findSettingsFile's fallback doesn't pick up stale data on the next restart. The user can delete the.orphanfile manually if no longer needed. - Fix:
.claude/settings.local.jsonfollows the user across flips. The file has no marker comment (it's JSON merged with user-added MCP permissions), so the marker-aware cleanup left it stranded at the old root. Migration now explicitly moves it if the new root doesn't already have one; if both exist, a WARNING tells the user to merge manually rather than blindly clobbering either copy. - Fix: legacy artifacts from prior Embody versions are swept on flip.
_cleanupOldRootFilesnow removes.claude/envoy-bridge.py(moved to.embody/in v4.x), root-level.envoy.json(moved to.embody/envoy.json),.embody.json(moved to.embody/config.json), and.envoy-tools-cache.json(moved to.embody/) at the old root — these would otherwise survive forever in long-lived installs and prevent.claude/fromrmdir'ing cleanly.
v5.0.407¶
Critical Windows-only crash fix introduced by the v5.0.402 registry GC: any path that re-registered the instance in envoy.json (Envoy toggle off→on, save, port change) silently terminated TouchDesigner with no Python traceback once the registry contained the running process's own PID. Root cause was Embody's _isPidAlive(pid) resting on os.kill(pid, 0) -- on Windows, CPython's posixmodule implements that as OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) + TerminateProcess(handle, sig) for all sig values including 0, so the "liveness check" literally told the OS to kill the process being checked. Plus a palette-scan timeline guard, an _verifyMcpImportable fast-path that stops tearing down 82 mcp.* submodules every toggle, and a bridge-side filter for TD's CEF/Web Render helper subprocesses that were flooding the bridge log.
- Fix:
_isPidAliveno longer terminates the process it's checking on Windows. CPython'sos.kill(pid, sig)on Windows routes throughOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)+TerminateProcess(handle, sig)-- there is nosig==0special case for liveness checking. Embody's_isPidAlive(added with the registry GC in v5.0.402) had been built onos.kill(pid, 0), so any time_writeEnvoyConfigiteratedinstancesand the iteration hit the running TD's own PID (which it does any time the project has been saved with Envoy enabled and the GC pass runs on the existing entry), the list comprehension at line 4774 calledTerminateProcess(self_handle, 0)and TD exited with code 0. Fingerprint: silent process death, no traceback, only on Windows, only when the registry holds an entry whosetd_pidmatches the running process, fresh projects unaffected because the registry hadn't accumulated. Confirmed end-to-end on the affected user's machine via a monkey-patched verify script: registry had exactly one row keyed to his own PID, the script'sabout_to_runfor that PID was the last entry written before TD vanished mid-call. Replaced with the safeOpenProcess(SYNCHRONIZE)pattern via ctypes (mirrorsenvoy_bridge.is_process_alive, in production indefinitely); SYNCHRONIZE access does not include termination rights -- the worst this can do is wait on a handle. Also defends against the secondary failure mode (OSError: [WinError 87]→SystemError: <class 'OSError'> returned a result with an exception set) that surfaces for registry entries whereOpenProcessreturnsINVALID_HANDLE_VALUEinstead of NULL; that path's WinError 87 leaves the interpreter thread state inconsistent and corrupts subsequent ticks. Strict input gate (isinstance(pid, int) and pid > 0) rejects None/string/bool/negative without a syscall; POSIX path catchesOverflowError/ValueErrorso corrupted-registry giants don't propagate. Also fixed the duplicate_os.kill(other_pid, 0)inside_findAvailablePort._port_registered_by_other-- same bug, would have silently killed any foreign live TD whose registry entry shared the port; rewired to call the shared safe_isPidAlivehelper. - Fix: palette-scan no longer pauses the timeline.
CatalogManager._processPaletteChunkdoeswrapper.loadTox()on every shipped palette.toxto map names to types, which runs each component's init code live. At least one shipped component on TD 2025.32820 (likely the refactoredPalette:logger v2.7.0, which now "parents with default TDAppLogger and internal loggers" on init, or the changedPalette:moviePlayer/Palette:movieEngine) mutates global timeline state on init -- the existing_PALETTE_SCAN_BLOCKLISTonly coveredtdvr/autoui._startPaletteScannow snapshotsme.time.play,me.time.rate,project.cookRate,project.realTimebefore queueing the scan;_processPaletteChunkrestores any of those that changed after every chunk (so a misbehaving component can't leave the timeline paused for the rest of the scan either);_finalizePaletteScandoes a final restore. Behaviour-equivalent to expanding the blocklist component-by-component, without requiring us to identify the offender in every future TD build. - Fix:
_verifyMcpImportablefast-path on thesys.modulesteardown. The v5.0.393 implementation cleared everymcp.*fromsys.modulesand re-importedmcp.serveron everyStart()-- about 82 submodules per cycle, wasted work that runs on every Envoy toggle. (Initial hypothesis was that the teardown was the crash culprit via pydantic_core re-registration; direct repro disproved it, but the cleanup is still correct.) Now short-circuits whenmcp.server in sys.modules-- a previous Start in this session already imported it. Thedel/re-import branch remains for genuine first imports and recovery from prior failed imports (covered by the dedicated regression testtest_A03_only_mcp_in_modules_not_mcp_server_does_not_short_circuit). - Fix: bridge
find_all_td_pids()filters CEF / Web Render helpers.pgrep -f TouchDesignermatchesTouchDesigner Web Render.app/Contents/MacOS/TouchDesigner(and the CEF GPU/renderer subprocesses TD spawns under it) because they share the executable basename. CEF recycles those children every few seconds, flooding the bridge log with phantomNew TD process detected/TD process exitedentries on a ~10s cadence (observed in one session: 214,751 such entries across a 29 MB log) and triggering needless config re-reads every cycle. Added_process_cmdline(pid)helper and_is_td_helper_process(pid)that checks for"Web Render"and"--type="markers in the cmdline;find_all_td_pidsfilters them out alongside the existing bridge-self filter. Refactored_is_bridge_processto share the cmdline helper. Three identical copies updated in lock-step: dev source (dev/embody/envoy_bridge.py), deployed (.embody/envoy-bridge.py), and the textDAT template (dev/embody/Embody/templates/text_envoy_bridge.py). - Fix: OS label disambiguates Windows 11 from Windows 10. TD's
app.osVersionreports"10"on both Windows 10 and Windows 11 because they share NT kernel version 10.0; the only reliable discriminator is the build number (≥22000 = Windows 11).EmbodyExt._osLabel()(called fromexecute.py:init()) now probessys.getwindowsversion().buildand corrects the label so startup logs andget_td_infono longer mis-label Win 11 machines. Pure_resolveOsLabel(os_name, os_version, win_build)is isolated from TD globals for testability; macOS / genuine Win 10 / non-Windows pass through unchanged. - Fix:
execute_src_ctrl.pyreads/writesREADME.mdas UTF-8. README contains emoji; the locale-default codec crashed on Windows under non-UTF-8 console code pages. Pinned both the read and write toencoding='utf-8'. - Tests: 8 new in
test_envoy_registry(TestIsPidAliveSafety):_isPidAlivecontract -- zero/None/negative/string/bool/oversized PIDs all return False without raising, own PID returns True, definitely-dead high PID returns False. Pins the safe contract against the SystemError class of regression. - Tests: 6 new in
test_catalog_palette_scan(TestCatalogPaletteScanTimelineGuard):_snapshotTimeState/_restoreTimeStateround-trip pause/cookRate/realTime, restore is a no-op without a snapshot, restore is a no-op when nothing changed, snapshot captures every tracked key. - Tests: 3 new in
test_envoy_setup_environment(TestVerifyMcpImportableFastPath): fast-path returns True whenmcp.serveris loaded, sentinelmcp/mcp.server/mcp.typesmodule objects are NOT replaced on the fast path (the regression guard), half-loadedmcpparent withoutmcp.serverdoes NOT short-circuit so recovery still runs. - Tests: 2 new in
test_envoy_bridge(TestBridgeProcessManagement):test_find_all_td_pids_filters_helper_processes(Web Render +--type=cmdlines are skipped, real TD pid survives),test_is_td_helper_process_markers(marker detection directly). - Tests: new
test_os_label.py: covers_resolveOsLabel-- Windows 11 disambiguation by build ≥22000, Windows 10 pass-through, macOS pass-through, missingwin_builddefaulting to the raw label. - Diagnostics: three dev-only bisect helpers in
dev/embody/diagnostics/--diagnose_envoy_toggle_crash.py(breaks downStart()step by step),diagnose_envoy_toggle_crash_v2.py(breaks down_configureMCPClientbody step by step),verify_ispidalive_fix.py(installs the patched_isPidAliveas a monkey-patch in a running TD and exercises both old and new code paths against the live registry). All use a flush-before-each-step JSON write so a silent process death leaves the diagnosis intact on disk; used inline during root-cause analysis and kept for future debugging of this class of failure. - Test file count: 50 → 53. New files:
test_catalog_palette_scan.py,test_envoy_setup_environment.py,test_os_label.py.
v5.0.403¶
Hotfix for a one-line typo in the v5.0.402 rename-detection backstop.
- Fix:
EmbodyExt.Update()rename-detect usesself.my, notself.ownerComp: The new rename-detection block added in v5.0.402 referencedself.ownerComp.ext.Envoy.RefreshRegistry().EmbodyExtstores its owner COMP asself.my(line 82:self.my = ownerComp); onlyEnvoyExtusesself.ownerComp. I copied the pattern from EnvoyExt without verifying. Result: everyUpdate()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 inspectingdev/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.jsonon 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._writeEnvoyConfignow scansinstancesand removes any row whosetd_pidis 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-timeRefreshRegistry, and the newUpdate()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()watchesproject.nameand triggersRefreshRegistry:execute.py'sonProjectPostSavealready callsRefreshRegistry, butexecute.pyis 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 ofexecute.py. New defensive backstop:Update()(which runs on every Refresh pulse and parameter change) caches_last_toe_nameand compares toproject.nameeach tick. On mismatch, it sets the new name and callsEnvoy.RefreshRegistry().EmbodyExtauto-reloads on source change, so this hook is reliable in a way that depending onexecute.pyreload isn't. Idempotent --_writeEnvoyConfigshort-circuits when the registry is already current. - Fix: bridge
launch_tdguard is PID-aware, not just key-aware: The v5.0.401 walk-forward inresolve_toe_pathinteracted badly with the v5.0.399 instance-specific guard. When the registry has a stale key (e.g. registered asEmbody-5.400even though the live.toeis now.401), the walk-forward correctly resolved the target toEmbody-5.401, then the guard looked upinstances["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 inhandle_launch_tdby adding a slow-path PID-aware scan after the fast-path key lookup: iterate every instance, skip dead PIDs, walk-forward each registeredtoe_path, and refuse if any resolves to the same target. Names the stale key in the error message so the user understands what toswitch_instanceto. 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_isPidAlivepredicate 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(registersProject-1.400with our PID, walks forward toProject-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 updateenvoy.jsonso the bridge can keep talking to the same TD process under its new basename. Two-part change inEnvoyExt._instanceKeyand_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 fromonProjectPostSaveinexecute.pyso 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 renamedEmbody-5.398.toe->Embody-5.399.toebut 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 helperfind_latest_versioned_toestrips the trailing<digits>.toefrom a missing path to derive a prefix, scans the directory for siblings, and returns the path with the highest extant numeric suffix.resolve_toe_pathnow reads frominstances[active]first (the multi-instance format -- previous flat-format-only behavior would silently return None for any modern config), falls back to legacy top-leveltoe_path, and walks the result throughfind_latest_versioned_toeso a stale registry entry still resolves to a usable file. The bridge does NOT rewriteenvoy.jsonfrom 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 readself.port, which is an attribute of the worker-threadEnvoyMCPServer, not the main-threadEnvoyExt. The actual runtime port isn't retained on the extension (it's a local inStart()), soRefreshRegistry()now reads it fromenvoy.jsonby looking up the row whosetd_pidmatchesos.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_instanceKeydirectly.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 inTestBridgeVersionIterationcoveringfind_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 inTestBridgeResolveToePathcoveringresolve_toe_path(multi-instance format, multi-instance walk-forward, legacy flat format, empty config, missing-active key). Plustest_launch_td_unrelated_td_runningproving the v5.0.399 instance-specific guard correctly allows launching alongside an unrelated TD project. - Tests: 1 updated:
test_launch_td_already_runningrewritten 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 matchesos.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.pyis 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_contentMCP tool — surgical text edits without round-trip cost:set_dat_contentis 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 newedit_dat_content(op_path, old_string, new_string, replace_all=False, confirm_wipe=False)tool mirrors Claude Code's Edit tool exactly:old_stringmust appear exactly once by default, otherwise the caller widens it with surrounding context for uniqueness or passesreplace_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 throughset_dat_content(rows=...)since string matching across cells is a different beast. Refuses emptyold_string(would match every position), refuses identicalold_string/new_string(no-op), and reuses the v5.0.397 wipe guardrail: edits that would leave the DAT empty requireconfirm_wipe=True. Not-found errors include diagnostics (DAT length, row count, case-insensitive hint) so the agent can self-correct without a secondget_dat_contentround-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_tdpreviously 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 (suggestsswitch_instanceinstead). Other TDs are now passed through cleanly. The macOS launch path also gained the-nflag (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 diffsfind_all_td_pids()against a pre-launch snapshot instead of returningpids[0], so the bridge correctly identifies the new TD's PID even with multiple TDs running.launch_td()(the helper) gained an optionalexisting_pidsparameter for this;handle_launch_tdsnapshots before delegating. - Test debt:
test_set_dat_content_clearupdated for v5.0.397 wipe guard: The wipe guardrail shipped in v5.0.397 added aconfirm_wipe=Truerequirement forclear=Truecalls with no replacement content, but the existingtest_set_dat_content_clearregression test was missed in that release's test sweep — it's been failing since v5.0.397 because it calledclear=Truewithout the new flag. One-line fix to addconfirm_wipe=True. Caught while running the newedit_dat_contentsuite. - 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), andtest_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_contentlisted in tool reference:docs/envoy/tools-reference.mdanddocs/envoy/index.mdadd 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 recommendedit_dat_contentfor partial edits and reserves itself for tables, full rewrites, and intentional wipes..claude/rules/skill-prerequisites.md, thetd-api-referenceSKILL description (and template), andtext_claude.md's tool-loading checklist all addedit_dat_contentalongsideexecute_pythonandset_dat_contentsince 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 withEnsureCatalogs()on fresh-drop: When a user drops the release.toxinto a brand-new project that has no.embody/catalog_<build>.jsoncached,CatalogManagerExt.EnsureCatalogs()kicks off a background scan and setsEmbody.par.Status = 'Scanning defaults (X/N)'to show progress. That scan runs concurrent with the post-onCreateVerify → UpdateHandler → Update → _promptEnvoychain. The chain sets_pending_envoy_prompt = TrueinVerify, thenUpdatewas supposed to consume it and schedule_promptEnvoy. ButUpdatehadif 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()andReconcileMetadata()) fromStatus != 'Enabled'toStatus == '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_scandirectly reproduces the race (sets Status='Scanning defaults', sets_pending_envoy_prompt=True, calls Update, asserts the flag was consumed).test_update_skips_only_when_disabledverifies 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_wipeguardrail onset_dat_content: The MCP tool is full-replace by design, but agents occasionally call it with emptytext="", emptyrows=[], orclear=Truewith 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 passesconfirm_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 toget_dat_contentas 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 explicitconfirm_wipe=Trueoverride 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_TYPESdenylist 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 fromdeform_info,keystone_info, andwebrtc_datoperators on every save. - Fix:
.embody/config.jsonis now byte-stable across saves (issue #18):_PERSISTED_PARAMSis a frozenset and Python's per-process hash randomization gave each TD session a different iteration order._saveSettingsused that order to populate the params dict andjson.dumpspreserved insertion order, so the file got a different (but valid) key ordering every session — producing a noisy diff on everygit statuseven when no settings changed. Two surgical changes inside_saveSettings: iteratesorted(self._PERSISTED_PARAMS)and passsort_keys=Truetojson.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
.txttest files removed: Pre-existing duplicate test files alongside their.pycounterparts indev/embody/unit_tests/, leftovers from an earlier externalization format. Not referenced by any DAT orexternalizations.tsventry, but the test runner discovered both.pyAND.txtfrom 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.txtwas byte-identical to its.py(or stale). - Test debt:
test_ancestor_renametearDown leak fixed (4 tests): All four "should succeed" assertions in_handleAncestorRenamewere failing intermittently because the test'stearDowncleaneddev/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_handleAncestorRenameand the test failed even though production code was working perfectly. NewtearDownsnapshots top-level dirs indev/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:
TestBridgeV2DeferredStubscarried threeraise 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 forfind_all_td_pidsfiltering (excludes own PID, excludes bridge processes, returns[]onTimeoutExpired/FileNotFoundError/ pgrep no-match). Deleted the 3s-probe stub entirely — already covered bytest_tools_list_bridge_only_when_td_downandtest_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, andtest_V12_mixed_network_no_interferencewere written against the old palette-detection model where native widget COMPs (buttonCOMP,sliderCOMP) cloned from/sys/TDTox/defaultCOMPs/were tagged with thepalette_cloneflag and had their children stripped from export. Production behavior was intentionally changed (commite759b89) to excludedefaultCOMPs/*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 (nopalette_cloneflag,childrenexported,clonereference captured in per-op params,enablecloningcorrectly omitted as it matches its default). Section header docstring updated to describe the current model. Thepalette_cloneflag remains reserved for true user palette clones from/sys/TDBasicWidgetsand similar. - Tests: 23 new tests across 4 files: 11 in
test_mcp_dat_content(wipe guardrail), 3 intest_tdn_safety_guards(TD-managed DAT filter), 3 intest_settings_persistence(issue #18 regression coverage — byte-stability + key sorting), 6 intest_envoy_bridge(ping handler + find_all_td_pids), minus 1 deleted stub. Test file count goes from 48 → 49. - Docs:
set_dat_contentand at-risk filter behavior:.claude/skills/mcp-tools-reference/SKILL.md,dev/embody/Embody/templates/text_skill_mcp_tools_reference.md(template counterpart), anddocs/envoy/tools-reference.mdupdated to surface the newconfirm_wipe?parameter with the wipe-guard contract.docs/embody/externalization.mdContent 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._setupEnvironmentpreviously returnedNoneon every path including four silent-return failure paths (uv not findable, mcp version metadata unreadable, twotry/exceptswallowed-error paths).EnvoyExt.Startcalled it fire-and-forget and proceeded to_runServerregardless, so any setup failure dropped the user intoRuntimeError: MCP server failed on port 9870: No module named 'mcp.server'with no indication of why._setupEnvironmentnow returnsbool; each previously-silent return path logs an actionable message with platform-specific hints (e.g. "macOS GUI apps do not inherit shell PATH" whenshutil.which('uv')comes up empty).Start()checks the return value, setsEnvoystatus = 'Error: Python environment not ready', logsAborting Envoy start -- See textport above for the underlying failure, and returns before_runServerruns. The other call site at_writeMCPConfig's venv-corruption recovery path is intentionally left ignoring the return — that path is already defensive with subsequentis_file()checks and exception fallback to system Python. Reported by Diego Chavez (issue #17). - Fix: final
import mcp.servergate catches partial installs: New_verifyMcpImportable()helper runsimportlib.import_module('mcp.server')after the install step succeeds — a populatedsite-packagesis 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 failedmcp/mcp.*entries fromsys.modulesbefore retrying so a TD-process import attempt that previously failed gets a clean re-evaluation. On failure it logsDependencies 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 theStart()gate. Catches the exact symptom Diego would have seen if his bootstrap had ever gotten past the empty-site-packagesfailure - Verified locally: Ran the exact bootstrap subprocess sequence (
uv venv .venv --python <TD-bundled-python>followed byuv pip install "mcp>=1.26.0" "attrs<25" --python .venv/bin/python) against TD'sPython.framework/Versions/3.11/bin/python3.11— both succeed cleanly on macOS Sequoia / Apple Silicon, 20 packages land insite-packages, prebuilt wheels load fine inside TD's process. Confirmedcodesign -d --entitlements :- /Applications/TouchDesigner.appshowscom.apple.security.cs.disable-library-validationis set on the host process (the framework's standalonepython3.11binary has no entitlements, which is why running.venv/bin/pythonfrom 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,cryptographyon 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.runfrom inside TD no longer raises[WinError 50]on Windows: Affected machines saw Embody's venv-bootstrap and verify-venv subprocess calls fail withOSError: [WinError 50] The request is not supported, traced tosubprocess._make_inheritablecalling_winapi.DuplicateHandleon the parent'sSTD_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 inEnvoyExt._writeMCPConfigtreatsOSErroras "venv corrupt" and runsshutil.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 nomcppackage and a crashing MCP server. Fixed by passingstdin=subprocess.DEVNULLon everysubprocess.runin the bootstrap path (3 sites inEmbodyExt._setupEnvironment/_findOrInstallUv) and the verify-venv path (2 sites inEnvoyExt._writeMCPConfig) — routes throughNUL, 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 withstdin=subprocess.DEVNULLreturnedrc=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.jsonbuild pin: New committed metadata file (sibling of the existing gitignored.embody/envoy.json) recordstd_build— the TouchDesigner version the project was last saved with.EmbodyExt._writeProjectJson()writes it ononProjectPostSaveand once at startup (onStartframe 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.pynow globs platform-specific install locations (C:\Program Files\Derivative\TouchDesigner.*on Windows,/Applications/TouchDesigner*.appon macOS viaInfo.plistCFBundleShortVersionString,/opt/derivative/touchdesigner-*on Linux) and picks the install matchingproject.json'std_build. Match policy: exact build → same year closest build (warns) → fall back toenvoy.json'std_executable(warns) → newest installed (warns) → error with download link. Backward compatible — projects withouttd_buildusetd_executablefromenvoy.jsonexactly as before. - Gitignore:
.embody/project.jsonis tracked:_configureGitignoreswitched the managed entry from.embody/to.embody/*+!.embody/project.json. Existing projects auto-migrate on next Embody startup — the bare.embody/line is added toSTALE_ENTRIESand replaced with the negation pair. Project's own root.gitignoreupdated to match. - Fix: MCP update-check no longer trips TD thread conflict:
_checkMCPUpdate()spawned a worker thread that calledself.Log()directly on update detection —Log()readsabsTime.frame, readsself.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,_setupEnvironmentruns twice (Start path plus venv-recovery), so two workers race to log "MCP update available" and the loser trips the dialog. Fixed by capturingowner_pathoutside the worker, pre-formatting the message string in the worker, and marshaling theLogcall to the main thread viarun("o = op(args[0])\nif o: o.Log(args[1], 'WARNING')", owner_path, msg, delayFrames=1). Theif 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.pywas carrying assertions left behind by prior bridge refactors.TestBridgeForwardToHttp(17 tests) expected a pooledhttp.client.HTTPConnection(bridge._http_pool,bridge._http_pool_lock,_get_http_connection); the bridge had long since been simplified to a freshurllib.request.urlopenper call. setUp /_make_connhelper replaced with_make_response; tests now mockurllib.request.urlopendirectly and inspect theurllib.request.Requestobject.TestBridgeLog.test_log_includes_prefixwas 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 onwait_for_envoyfor arbitrary methods; v2 triesforward_to_httpimmediately and only errors when the forward call itself raises — tests now mockforward_to_httpto raiseOSError. 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
TestBridgeProjectJsonAndDiscoveryclass (15 tests) intest_envoy_bridge.pycoversload_project_config()for missing / valid / malformed / non-dict cases,_parse_build()for valid / embedded / invalid inputs, andselect_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 theinstalls=injection point. Discovery sanity-checked live on the dev machine — picked up the installed2025.32460build, and_writeProjectJsoncorrectly wrote the pin to.embody/project.jsonononStart. - Dev rules: release-save procedure:
.claude/rules/release-commits.mdgained a new "Step 0: Save the Project" section documenting thatproject.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 currentproject.path, desyncing the.toefilename suffix frompar.Versionby one. Pre-settingpar.Versionmanually has the same effect through a different route. Section also covers recovery if you've already mis-saved (rename.toeon 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 singleDuplicate Paths Detecteddialog appears with three choices:Dismiss(skip for now, re-prompt next cycle),Review individually(falls back to the existing per-group prompt per group), orAuto-resolve all (N)(picks the first listed operator in each group as master; tags the rest withclone). 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:
_messageBoxlist-of-responses for headless testing: The test harness's_smoke_test_responsesstorage 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 newReview individuallypath where the per-group prompt fires multiple times within a singlecheckForDuplicates()call. Backward compatible — existing single-int seeds still work - Tests: New
TestBatchResolutionclass intest_duplicate_handling.pywith 6 tests covering the single-group shortcut, each batch-prompt button, per-group fallthrough, and the_autoResolveFirstAsMasterhelper including empty-input safety. Full duplicate_handling suite: 56/56 passing. Verified live in the dev project with 3 fake groups —Auto-resolve allcorrectly keptalpha_1,beta_1,gamma_1as 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()andisInsideClone()in EmbodyExt were misclassifying reusable-component masters whosepar.cloneevaluates to themselves (a standard pattern — a component COMP setspar.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 treatpar.clone is self(identity check on the evaluated op) as a master, not a clone.isClone()simplified from "doesoper.nameappear in the stringified clone value" string-match to the direct identity comparison. Added three unit tests intest_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/▾ Namestring, which left the path column doing two jobs and misaligned when names varied in length.list_callbacks.pynow 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:
.gitignoreentry 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
Performmodetoggle 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,_enterPerformModesnapshots pre-state (Envoy running, keyboard listener active, exit tagger active) and stops Envoy directly, disables thekeyboardin1DAT andchopexec_exit_tagger, closes the manager window, greys out Envoy parameters, and setsEnvoystatus = 'Perform Mode'. Guards added toUpdate,Refresh,Save,SaveTDN,SaveCurrentComp,TagGetter,ExternalizeProject,getDirtyCount,onProjectPreSave,onProjectPostSave, and Envoy's_onServerSuccess/_onServerErrorauto-restart._exitPerformModerestores snapshot state and restarts Envoy if it was running.execute.py:onCreateclearsPerformmode = Falseon project open so the toggle never persists across sessions. Envoy parameter changes toEnvoyenable/Envoyport/Aiclientare 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 incleanupAllDuplicateRows()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./old→old) and passing that straight toproject.folder / old. That works by coincidence whenExternalizationsfolderis empty (the default — files write directly under the project root) because the op-path segment and the on-disk segment match. The momentExternalizationsfolderis pointed at a subfolder (sayext/), files actually live atproject.folder / ext / old / ...but the rename code was still looking atproject.folder / old / ...— soold_dir.exists()returned False and the user saw "Source folder not found." The method now composesExternalizationsfolderinto 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). ReturnsboolsocheckOpsForContinuity()can fall back to per-operator handling when the ancestor-level rename fails for any reason - Hardening: Clone detection null-safety:
isInsideClone()andisClone()now usegetattr(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
performtextCOMP (Material Design icon) between Status and Disable buttons, wired toToolbarExt._action_toggle_perform(). Tinted amber when active (face color driven byPerformmodeparameter, matching the Disable button's active-state pattern). Keyboard shortcut suppression, exit-tagger gating, and parexec routing to_enterPerformMode/_exitPerformModeall fire from the single Performmode toggle - UI: Full Envoy status string in toolbar:
envoy_statuswidget now reads the fullEnvoystatusparameter ("Running on port 9870","Off","Error: ...","Perform Mode") instead of just the port number. Width expanded 55 → 160 units. Text color unchanged (uses defaultTextcolor, not green). Window headermin_widthbumped 410 → 440 to accommodate the new button; title now concatenatesHeaderlabel + ' · ' + Envoystatusso 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.mdand expanded "TOP Pixel Access" inskills/td-api-reference/SKILL.mddocumenting TD's bottom-left origin convention.TOP.sample(x, y)y=0 is the bottom edge, GLSLgl_FragCoord.y=0is the bottom, UV and crop/transform params are bottom-left, butTOP.numpyArray()returns rows top-to-bottom and PIL/OpenCV/panel coords are all top-left. Table +np.flipud()guidance added. Templates indev/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 usinglen(clones) != 1as the plural guard, producing "0 DATs" with an errant s in the no-clones path. Changed tolen(clones) > 1 - Test: 48 test suites (+1): New
test_ancestor_rename.py(680 lines) covers_detectAncestorRenamethreshold and prefix extraction,_handleAncestorRenamePhase A/C/D on externalized COMPs, disk segment composition withExternalizationsFolder, 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.pyexpanded (+70 lines) to cover_buildPathGroupsreplicant filtering,_resolveClonesByCloningAPInon-COMP handling,_resolveDATsInClonedCOMPsauto-tagging, and dialog-driven master/clone selection.test_tag_management.pyexpanded (+57 lines) to coverisInsideClonenull-safety on DATs withoutpar.cloneandisCloneactive-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) beforeloadTox. Whenpalette_catalogbootstrap doesn't cover the current TD build, the runtime scan used to load every palette .tox into a hidden workspace — including TDVR (which unconditionally callsproject.cookRate = 90and 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.tsvnow covers build 099.2025.32460: RanExportPaletteCatalog()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, noloadToxcalls, 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+enablecloningboth 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 fromchild.nametochild.pathso 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:
Tdnmodethree-way menu: NewTdnmodemenu 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.tdnfiles on save for diffs and AI context, but does not rebuild COMPs from.tdnon open — the.toeremains 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). InternalmenuNamesstay asoff/export/fullso persisted values and code references don't churn - Feature: Migration nudge for upgrading users: On first open after upgrade, projects saved with the legacy
Tdnenabletoggle 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_restoreSettingsis called twice within the 60-frame defer window - Feature:
read_tdnMCP 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 viaget_op+query_networkthanks to default omission,type_defaults, andpar_templatescompaction. Intended as the preferred read path for LLM workflows exploring networks of more than ~3 operators. Works in all threeTdnmodevalues (reads live state, not disk). Scope cost viacomp_path; cap withmax_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 ANDcomp.storagefor at-risk user data inside TDN-strategy COMPs, surfaced in one combined dialog._findAtRiskStoragemirrors_findAtRiskDATs. Parameter renamed from "DAT Safety" to "Content Safety" to reflect the expanded scope._STORAGE_SKIP_KEYScovers 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
Tdndatsafetyparameter 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 likebuttonCOMPas 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:
onProjectPostSaveregression in Off/Export modes: Post-save used to early-return when_tdn_stripped_pathswas empty — which never happened in theTdnenable=Trueworld but happens on every Off/Export save. The early return skipped_init_completere-store, silently disabling every parexec callback for the rest of the session. Strip-restoration is now guarded byif stripped:; pane restore,_init_completere-store, and the delayedRefresh.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
exportnow 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 whenTdnmode = 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.mdreplaces the binaryTdnenablenarrative 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.tdnfile sizes.configuration.md,getting-started.md,troubleshooting.mdupdated to match. In-app help text onTdnmodeandTdndatsafetyrewritten.read_tdnadded 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.htmlTDN Strategy feature card reframes TDN as a mirror of the.toerather than a replacement. The "bidirectional sync" pillar becomes two pillars — export on save (the default) and roundtrip (experimental).web/tdn/index.htmlEmbody pillar softened to match.web/envoy/index.htmladds a newread_tdnfeature card highlighting the 20-90× token reduction; tool count updated from 46 to 47. Sample TDN JSON generator strings bumped toEmbody/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_completeand Envoy restart),test_tdn_safety_guards(7 tests covering_findAtRiskStorage, combined dialog, Never-Ask removal, skip logging),test_mcp_tdn_tools(5 tests coveringread_tdnround-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 viacomp.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 fromexecute.py:onCreateat frame 45 in addition toonStart. Previously new users dropping the.toxhad 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 alevelparameter. 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:
_processChunkand_processPaletteChunknow finalize the scan when the queue empties instead of relying on a scheduledrun(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_catalogtableDAT portability: Both the DAT'sfilepar and the row inexternalizations.tsvhad an absolute path (broken on other machines). Now uses relativeembody/Embody/palette_catalog.tsv+syncfile=True+file.readOnly=True, matching thedivergent_defaultspattern - 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.jsonremoved 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, avoidingCheckAndX/DoStuff/implementation-leakage patterns, booleanis/has/canphrasing, public-vs-private conventions - Docs: Updated configuration.md, externalization.md, TDN specification.md, in-app help text, and
externalize-operatorskill to cover palette handling and the shipped palette catalog mechanism - Test: 44 test suites (+1, 10 new G01-G10 tests in
test_tdn_palette_catalogcovering 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:
CatalogManagerExtnow walks TD's shipped palette directory after the op-type scan, loads each.toxinto 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 coversTDBasicWidgetsin addition toTDResources/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 theinclude_dat_contentoption. 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.StripCompChildrennow captures external wires viacomp.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_completeimmediately after settingEnvoyenable = False, but TD defersonValueChangecallbacks to the next cook — so parexec processed init's ownEnvoyenable=Falsechange and calledStop()._init_completeis now stored by_restoreSettingsafter restoration completes (or immediately on its early-return paths), keeping parexec suppressed through the deferred callbacks - Fix: Catalog path mismatch:
CatalogManagerExt._findProjectRoot()now delegates toEmbodyExt._findProjectRoot()which walks up fromproject.folderlooking for.git. Previouslyproject.folder(oftendev/) 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_FAMILIESbut aren't creatable. Added_ABSTRACT_TYPESfilter 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..gitignorestale 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_countstuck 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 whenenvoy_runningis already False - Fix: Upgrade-path port race:
Verify()deferredStart()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'sp.defaultlies for dozens of parameters (e.g., cameraCOMPtz:p.default=0but creation value is5). 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.tsvtable provides bootstrap data for known TD builds. On-the-fly probing handles unknown builds by creating temp operators and comparingp.valvsp.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) andPeekNamedPipe(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.urlopenper call) — the pool caused persistent "not responding" errors from half-closedhttp.clientconnections - 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
ClientDisconnectto suppressed exceptions alongsideBrokenResourceError/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_operationsMCP 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.
onProjectPostSavenow 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_operationsorexecute_pythoninstead - 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.jsonevery 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.jsonso new sessions always start with all 45+ tools, even if TD hasn't finished loading. Works around Claude Code'slist_changednotification bug (#13646) - Feature: HTTP connection pooling: Replaced per-request
urllib.request.urlopen()with a persistenthttp.client.HTTPConnectionper URL. Eliminates socket churn that was causingClientDisconnecttracebacks 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_changedemission: Bridge advertiseslistChanged: trueand sends the MCP notification on every backend state transition and explicit instance switch - Feature: Local
pinghandler: Answers MCPpingrequests 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.jsonregistry 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
Envoyportparameter update duringStart()that triggeredparexec.pyStop+Start cycle when the port shifted (e.g., 9870→9871) - Fix: Ghost TD detection:
find_all_td_pids()now excludes bridge processes whose cmdline containsenvoy-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/listresponse 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 PingRequestmessages 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
sequenceskey 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 asequencefield; on import,blockSizeis set from the template par count beforenumBlockspopulates the block instances. Includes a fallback resolver for custom-sequence block parameters where TD'sblock.par.{base}lookup returnsNone - Feature: Read-only DAT detection: Auto-generated companion DATs (e.g.
glsl1_info,popto1) that rejectdat.text = ...writes are now probed at export time and tagged withdat_read_only: true. Their content is excluded from the export, and importers no longer log "not editable" warnings when restoring them. Older.tdnfiles 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.,comb2operon 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 aftercreate_op - Test: Sequence round-trip tests: 12 new tests in
test_tdn_sequences.pycovering 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.pycovering 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_completewas stored as an instance attribute on EmbodyExt, destroyed when file sync recompiled DATs during first-time setup. Parexec silently dropped all parameter changes — includingEnvoyenable = True— soStart()was never scheduled. Moved_init_completeto 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__setEnvoystatus = 'Starting...'before deferringStart()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:
.gitignoreand.gitattributesnot generated on first-time git init (GitHub issue #8): Git config files are now created inside_checkOrInitGitRepo()immediately aftergit initsucceeds, instead of relying onStart()which may not run in the same session - Fix: Type error in
Start()git config (pre-existing):_configureGitignore/_configureGitattributesexpect aPathobject butStart()passed a string from COMP storage. Wrapped withPath()conversion - Improved:
Start()status guard visibility: Upgraded theEnvoystatusbackup 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 sameexternaltoxpath would trigger a massive popup with 100+ buttons. Added_resolveReplicants()safety net that auto-tags replicants as clones without prompting if they reachcheckForDuplicates()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.tdnsnapshot in one step - Improved: TDN export
source_filefield: All TDN exports now include the originating.toefilename 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.tdninstead ofEmbody-5.305.tdn) - Test: Replicant handling tests: 4 new tests in
test_duplicate_handling.pycovering_resolveReplicants,isReplicant, and replicator integration with_buildPathGroups - Test: TDN file I/O tests: 7 new tests in
test_tdn_file_io.pyfor_stripBuildSuffixedge cases andsource_fileexport 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.cloneAPI 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 atPath.home(), preventing accidental discovery of unrelated git repos (e.g. dotfiles in~). Added_git_prompt_activeguard against concurrent git dialogs - Fix: Envoy auto-start on fresh .tox drop:
EnvoyExt.__init__was schedulingStart()based on the bakedEnvoyenable=Truebeforeinit()could reset it. Added_init_completeguard so auto-start only fires during extension reinit in a running session, never on fresh install. Removed_setupEnvironment()fromEmbodyExt.__init__(now runs insideStart()) - Fix: Envoy opt-in prompt not appearing:
_restoreSettings()finding a leftover.embody.jsoncausedVerify()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:
onProjectPreSavenow 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
SyntaxErrorwhen TD reads externalized files with CP1252 encoding - Improved:
_restoreSettings()kick_envoy parameter:onStart()passeskick_envoy=Trueto defer Envoy start after settings restore;Verify()(onCreate path) uses defaultkick_envoy=Falsesince it owns the Envoy startup flow - Test: Duplicate handling tests: 5 new tests in
test_duplicate_handling.pycovering_buildPathGroups,_resolveClonesByCloningAPI, group dialog, and user-selects-master flow - Test: Smoke release fix:
test_envoy_server_running_if_enablednow checksEnvoystatusparameter (survives extension reinit) instead ofenvoy_runningstore - 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 toproject.folderwhen the previous folder was empty, thendeleteEmptyDirectorieswould walk the entire project tree and delete the newly-created target directory.UpdateHandlerthen failed withFileNotFoundError. Fixed by guarding all directory cleanup to never operate onproject.folder, and switchingos.mkdirtoos.makedirs(exist_ok=True)for robustness - Regression tests: Two new tests in
test_custom_parameters.py—test_zz_folder_10_empty_dir_survives_disablereproduces the exact issue #3 scenario,test_zz_folder_11_disable_empty_prev_skips_project_folderverifies 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_heightfrom 72 to 66 to eliminate black bar at bottom of minimized manager (header 26px + toolbar 40px = 66px exactly). Increasedmin_widthfrom 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
.tdnfiles on disk now mandates an immediateimport_networkMCP call to reload in TD - Manager toolbar docs: New toolbar button reference table added to
manager-ui.mdwith 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 TDNandExport Current COMP to TDNread-only parameters to the UI custom page, displaying thectrl/cmd + lshift + eandctrl/cmd + alt + eshortcuts alongside the existing four shortcut pars - Keyboard shortcuts docs: Added an info callout to
keyboard-shortcuts.mdclearly 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.jsonfile 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.toxupgrades, crashes, and force-quits. Whitelisted parameters include folder, Envoy config, tag names, tag colors, TDN settings, and logging options. Restore runs silently (noonValueChangeside effects) via_restoring_settingsflag - Crash-safe restore:
_restoreSettings()runs at frame 5 on every project open, not just on fresh install. If the.toehas stale values (unsaved session, crash),.embody.jsonwins - Extension initialization timing docs: New documentation covering the critical
onInitTD/ TDN import timing issue — extensions inside TDN COMPs must defer initialization becauseImportNetwork(clear_first=True)overwrites any state set duringonInitTD. Added totd-python.mdrule,create-extensionskill,extensions.mddoc, and TDN specification - Template sync: Updated
text_rule_td_python.mdandtext_skill_create_extension.mdtemplates 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) calledappendXXX(replace=True)on palette clone operators (annotateCOMP), destroying internal parameter bindings that the clone's rendering network depends on — fix: skip Phase 2 forpalette_cloneoperators. 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.0from 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_refcross-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.pyadded 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
Tdndatsafetyparameter stores the user's preference. Called fromonProjectPreSave()before TDN export - Palette clone parameter fidelity: TDN export now compares parameters against both
p.defaultand the clone source's actual value. Parameters that matchp.defaultbut differ from the clone source are preserved, fixing silent data loss on rebuild (e.g.,buttontypedefaulting to"momentary"when clone source is"toggledown").clone/enablecloningparameters 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:
EnvoyExtnow validates that the.venvPython actually executes before using it for the bridge. Catches stalepyvenv.cfgpointing 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
Envoyinstancenameparameter 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
Envoyinstancenamereferences 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.pynow 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_positionfor 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 /localpath prohibition: New critical rule (#3 in CLAUDE.md) and step 1 in the create-operator workflow: agents must NEVER create operators under/localor/local/*. The/localstorage is volatile and not saved with the.toefile. Agents must place operators under the project root or useui.panes.current.owner.pathto 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.jsonentries, 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 (
basethroughbase+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.toefile is opened in multiple instances, keys are auto-suffixed (MyProject,MyProject-2, etc.). Stale entries with dead PIDs are reclaimed automatically Envoyinstancenameparameter: Optional custom name for the Envoy instance registry. Overrides the auto-generated key from the.toefilename — useful for predictableswitch_instancetargetsswitch_instancebridge 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 fromStart()into a dedicated method. Replaces the recursive retry loop with a clean single-pass scan- Atomic JSON writes: New
_atomicWriteJSON()method for.envoy.jsonwrites — uses temp file +os.replace()with WindowsPermissionErrorretry to prevent corruption under concurrent access - Graceful shutdown via MCP: Documented
project.quit()as the preferred way to close TD instances programmatically — triggersonDestroyTDfor clean deregistration - Multi-instance documentation: New
/multi-instanceskill, updated architecture docs, setup guide, Claude Code integration docs, troubleshooting entries, and tools reference. All surfaces documentswitch_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 callsTerminateProcess(), killing TouchDesigner instead of checking liveness. Everyget_td_status,launch_td, andrestart_tdcall terminated TD on Windows. Now usesOpenProcess(SYNCHRONIZE)via ctypes on Windows, preserving the Unix signal-0 path for macOS/Linux - Reconstruction verification fix:
_verifyReconstructedComp()accessedchild.errorsandchild.warningsas 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_alivetests (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
childrenarray is now skipped during import. The child COMP shell is still created, but its internal network is left to its own.tdnfile — 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.tdnfile - Import input validation:
ImportNetwork()now validates thatoperatorsis a list, returning a clear error instead of failing cryptically on malformed input - Material reference test fix: Corrected
test_T07_geometry_material_roundtripto use./my_mat(child reference) instead ofmy_mat(sibling reference), which was unresolvable from inside the geometryCOMP assertAlmostEqualadded to test framework: TestRunnerExt now supportsassertAlmostEqual(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:
_wireConnectionListnow bounds-checksinputConnectorsbefore indexing and falls back toinputCOMPConnectorsfor 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.
_messageBoxauto-response system: Dialog calls can be intercepted by seeding_smoke_test_responsesin 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
Filecleanupparameter (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.
torus1inside 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:
ExportPortableToxnow strips the target COMP's ownexternaltox/enableexternaltoxparams (not just descendants) and handles thesyncfileparameter, preventing baked-in references from confusing recipients - Bridge
project_pathoverride:launch_tdandrestart_tdmeta-tools accept an optionalproject_pathparameter to open a different.toefile, resolved relative to the git root - Envoy start deferred:
parexec.pydefersStart()by 5 frames soonCreatehas time to suppress baked-inEnvoyenable=Truebefore the server launches - SCM directory protection:
deleteEmptyDirectoriesand_cleanupFoldernow skip.git,.svn, and.hgdirectories - Cross-platform temp paths: All Envoy temp file operations use
tempfile.gettempdir()instead of hardcoded/tmp findChildren()fix: Two calls using invaliddepth=-1corrected tofindChildren()(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.toefor E2E release testing - New tests: 22 smoke release tests (post-init state,
_messageBoxmechanism,_promptEnvoyauto-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, andstorageat 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()andImportNetworkFromFile()now setui.statuson 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.jsonfor 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 initfails during Envoy setup, aui.messageBoxnow 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 thetypefield from v1.1.tdnfiles 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_tdbridge meta-tool: Gracefully quits TouchDesigner and relaunches with the project's.toefile. 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, andtools/listlocally 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_positionoverlap warning: After repositioning an operator, EnvoyExt checks for bounding-box overlaps with siblings (20-unit margin) and returns anoverlap_warningfield naming the conflicting operators- Layout rules hardening: Network-layout and create-operator rules now require dimension-aware spacing (
nodeWidth/nodeHeightfromget_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.jsonconfig reference, and meta-tool catalog
v5.0.233¶
Project-level performance monitoring, pre-handoff validation, Envoy bridge hardening, test runner dialog fix.
get_project_performanceMCP 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/validatecommand: 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:
Filecleanupparameter is now suppressed todeleteduring 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
_runningflag isn't active (handles reinit, between-suite gaps, post-failure) - Envoy bridge hardening:
.envoy.jsonproject 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_errorsnow returns warnings: The MCP tool calls bothOP.errors()andOP.warnings(), returning structuredwarnings/warningCount/hasWarningsfields 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.tdnand 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_tdnnow writes via temp file +os.replace+fsyncto prevent partial writes corrupting.tdnfiles on crash or power loss - Backup rotation: Before each write,
.tdnfiles are copied to.tdn_backup/(.bakand.bak2generations)..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:
ReconstructTDNCompsandonProjectPostSavenow attempt rollback from.bakif 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:
Refreshnow detects structural changes in TDN-strategy COMPs (not just parameter changes) and triggersSaveTDNwhen children are added/removed/renamed - About page filtering:
Build,Date, andTouchbuildparameters are excluded from TDN export and reconstructed fromexternalizations.tsvat 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:
_writeTemplatenow 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.pyadds_tdn_content_equaland_read_existing_tdntests;test_tdn_reconstruction.pyadds 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_externalization→externalize_opacross 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_opis a single-step operation (no separatesave_externalizationneeded), and thatsave_externalizationis 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, andbodyFontSizeannotation parameters, preserving non-default values through TDN round-trips - GitHub release rule: New
.claude/rules/github-release.mdwith post-push workflow for detecting release artifacts, extracting version from changelog, and creating GitHub releases viaghCLI. Added toEmbodyExt._TEMPLATE_MAP_RULESfor auto-deployment, template synced, release-commits.md updated with mapping - Templates TDN cleanup: Annotations in
templates.tdnnow use the native annotation format instead of being represented as annotateCOMP operators. RemovedannotateCOMPtype defaults andpar_templatessection. 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.mdenforcing change evaluation, doc audit, test audit, and release detection before every commit. Added toEmbodyExt._TEMPLATE_MAP_RULESfor 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
helptooltip text. TDN schema and specification updated with the newhelpfield - 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
Filecleanupparameter - Dock safety on destroy: Both
_clearChildren(EmbodyExt) and TDN import (clear_first) now clearchild.dock = Nonebefore destroying child operators, preventing uncatchabletdErrorwhen a dock target is destroyed before its docked operator - Companion reuse fix:
_createOpsnow 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:
_ensureGitRepostripsGIT_DIR,GIT_WORK_TREE, and other git env vars beforegit initto prevent broken repos caused by TD's embedded Python environment. Verifies the init withgit rev-parseand retries on failure attrs<25version pin: MCP dependency install now pinsattrs<25to avoid conflicts with TD's bundledattrmodule. 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 acceptdelete_fileparameter to optionally preserve files on disk when removing tracking entries- Parameter rules: New dedicated
.claude/rules/parameters.mdcovering help text, sections, naming, ranges, styles, and page organization.td-python.mdnow points to it. TD API reference skill updated with post-creation property examples - New tests:
test_strategy_handlers.pywith 15 tests covering_removeExternalization,HandleStrategySwitch,_dispatchTaggerButton, and manage-mode button dispatch.test_mcp_externalization.pygains 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 byDatrestoreonstartparameter. 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
.tdnfiles — smaller diffs, cleaner hierarchy - base_test converted to TDN: Replaced binary
base_test.toxwithbase_test.tdnfor diffability - text_claude.md relocated: Template moved from
Embody/root intoEmbody/templates/alongside other template DATs - New tests:
test_dat_restoration.pywith 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.pyand settings template moved into the templates COMP for centralized management - Envoy startup resilience:
_upgradeEnvoy()failure no longer blocks MCP server startup .gitignoremanaged entries: Expanded auto-managed entries to includeBackup/,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-networkcommands to.claude/commands/for common workflows - CLAUDE.md deduplication: Moved rules that were restated in both
CLAUDE.mdand.claude/rules/into rules only — reduced critical rules from 15 to 9. Skill prerequisites moved to dedicatedskill-prerequisites.mdrule - Getting Started update:
.gitignoredocumentation 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_layoutMCP tool: Returns positions and sizes of all operators and annotations in a COMP in a single call — replaces the need for repeatedget_op_positioncalls. Includes bounding box calculationsave_externalizationfix: Now correctly handles TDN-strategy COMPs (callsSaveTDN) and file-synced DATs, instead of blindly callingSave()which only works for TOX-strategy COMPsSave()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:
_createOpsnow 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:
_exportChildrendetects and skips accumulated companion duplicates (e.g.timer1_callbacks1,timer1_callbacks2) using docking-based detection — existing.tdnfiles self-clean on next save
v5.0.204¶
Custom window header, path portability, TDN template cleanup.
- Custom window header: Replaced
widgetCOMPclone of TDBasicWidgets with a lightweightcontainerCOMP+WindowHeaderExtextension — 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_defaultsentries (baseCOMP, panelexecuteDAT, constantTOP, opexecuteDAT) and stale custom par pages (settings_2, expressions) from Embody.tdn — smaller, cleaner exports - EmbodyExt.py:
self.ownerComp.path→self.my.pathfor 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 runsVerify()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 parameterVerify()— two-scenario detection: Fresh install (empty table) runsUpdateHandlerquietly 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, andnode_colorcolumns. Existing tables are migrated automatically on first open - Release build:
execute_src_ctrl.pynow clearsTdnfileandNetworkpathpars beforeExportPortableTox()so the baked.toxdoesn't carry stale TDN paths into new projects
v5.0.190¶
Automatic restoration, documentation overhaul.
- Automatic restoration: TOX-strategy COMPs are restored from
.toxfiles and TDN-strategy COMPs are reconstructed from.tdnfiles on project open — users no longer need to save their.toeto 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+ssave 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.toxwith 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:
ImportNetworkFromFilenow returns structured error dicts instead ofNoneon failure - TDN per-COMP split: Refactored
_splitPerCompinto 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-lctrlnow shows an Actions menu for already-tagged operators (tag, retag, export portable tox, etc.) - Release build:
execute_src_ctrl.pynow usesExportPortableTox()instead of rawcomp.save()for portable release.toxfiles - Test fixes: Updated test_custom_parameters (synchronous
ReexportAllTDNscall, 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
.tdnfile exists before stripping children (prevents data loss) - Post-save tracks restore failures for retry on next project open
fileandsyncfileparameters 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
StripCompChildrennow 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_externalizationtype 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 (export/import) instead of TOX, enabling human-readable diffs
- Strip/restore save cycle: TDN-strategy COMP children are stripped before
.toesave and reconstructed from.tdnon project open, keeping the.toesmall - Compact TDN format:
type_defaultshoists shared parameter values,par_templatesdeduplicates custom parameter definitions, expression shorthand (=prefix for expressions,~for binds) - Per-COMP split export mode: large networks export as one
.tdnfile per COMP for git-friendly directory structures externalizations.tsvgainsstrategycolumn (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: 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.