Skip to content

fix(bridge): listener watchdog, fixed-time auth, agent value coercion, auto-tick - #182

Merged
yhc509 merged 4 commits into
devfrom
feat/bridge-hardening-autotick
Aug 14, 2026
Merged

yhc509 merged 4 commits into
devfrom
feat/bridge-hardening-autotick

Conversation

@yhc509

@yhc509 yhc509 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

#175 + #174 + #176 + the auto-activation half of #173.

What changed

#175 — fixed-time auth token compare. string.Equals on the token path (length test + early exit) is replaced by AuthTokenComparison.FixedTimeEquals. Hand-rolled rather than CryptographicOperations.FixedTimeEquals so the shared protocol source keeps compiling on Unity's runtime profile and .NET 9. Accept/reject behavior unchanged.

#174 — listener watchdog. The Unix accept loop ended permanently on any unexpected AcceptAsync failure and both loops gave up if the first acquire failed, while the heartbeat kept advertising the instance — a target the CLI resolves and then cannot connect to. BridgeHost now clears listener readiness when an accept loop ends and runs ListenerWatchdogPolicy (Unity-free, unit-tested) each editor tick: skip while a bind is in flight or the editor is compiling/updating, rebind in place up to 5× at 5 s, re-publish registry + token sidecar on recovery, and unregister the instance once the attempts are spent.

#176 — agent value coercion. ApplyToken re-parses a string token when the target cannot hold a raw string and the trimmed text opens { or [. Vector-family readers also accept a flat numeric array, which is what lets "[1,2,3]" actually reach a Vector3.

#173 (half) — auto-tick. Deferred flows subscribe their poll through EditorTickPump.Add/Remove instead of EditorApplication.update; the pump forces EditorApplication.SignalTick while any is subscribed and disarms when the set empties. The explicit editor auto-tick wire command and its protocol bump are not in this PR, so the wire protocol is untouched.

Measurements

Auto-tick, Unity 6000.3.10f1 / macOS GUI, 60 EditorApplication.update ticks via profile stats --frames 60:

pump off pump on
focused 9.3–9.5 s 6.0–6.1 s
unfocused 9.4–10.4 s 6.0–6.1 s

Two things worth flagging against the issue's premise:

  • The unfocused-vs-focused gap did not reproduce on this version — the editor idles at ~6 ticks/s either way. So "unfocused ≈ focused" is satisfied, but only because they were already equal; the real win is a uniform ~1.55× (≈160 ms → ≈100 ms per tick).
  • ~100 ms per tick is a floor I could not get under. Signalling from the update hook, from a 16 ms off-thread timer, Application.runInBackground = true, and InteractionMode = NoThrottling all land on exactly 100 ms. Interval 0 and 16 measure identically, so the default keeps the 16 ms throttle to cap the forced rate where the loop can run faster.

Verification

dotnet test: 1032 passed. Doc-gen --check and check-unity-meta.sh clean.

Live, against a Unity 6000.3.10f1 GUI editor with a file: reference to this package:

Not covered

A whole values blob sent as one quoted string ("values": "{\"m_Mass\":0.17}") is still rejected — the coercion is per-field. The JObject-typed spec models would have to become JToken first; happy to follow up if that shape shows up in practice.

🤖 Generated with Claude Code

yhc509 added 4 commits August 13, 2026 23:02
…alues

Agents routinely send a structured value as a JSON-encoded string —
"[1,2,3]" for a Vector3, "{\"m_Mass\":0.17}" for a nested object. Strict
token typing rejected those with an error that reads as though the value
itself were wrong, so the agent has no way to tell quoting from a bad
field.

ApplyToken — the single funnel for top-level, nested and array-element
values — now re-parses a string token when the target cannot hold a raw
string and the trimmed text opens a JSON object or array. The guard stays
narrow on purpose: asset paths, object-reference handles, enum names and
char values never start with '{' or '[', and a parse failure falls back to
the original token so the existing validation error survives.

The vector-family readers additionally accept a flat numeric array next to
the canonical member object, which is what makes "[1,2,3]" reach a Vector3
at all. Bounds/BoundsInt stay object-only — their nested center/size take
the array form instead of inventing a flat six-element convention.

Closes #176
Every deferred command — test runs, package operations, profiler sampling,
recording, qa run-sequence — advances one step per EditorApplication.update
tick, and an idle editor runs that loop at roughly 6 ticks/second. Forcing
the internal EditorApplication.SignalTick schedules the next tick
immediately.

Activation is automatic and scoped: deferred flows now subscribe their poll
through EditorTickPump.Add/Remove instead of EditorApplication.update, and
the pump runs only while at least one is subscribed. Because the subscriber
set holds the very delegates that are on EditorApplication.update, a pump
leak would be a poll leak — an invariant those flows already depend on —
and Remove is idempotent to match their defensive double-unsubscribes.
SignalTick is bound reflectively once; if it is gone the pump degrades to a
no-op with a single warning rather than taking commands down with it.

Measured on Unity 6000.3.10f1 / macOS with 60 update ticks: 9.4-10.4s
without the pump, 6.0-6.1s with it (~1.55x), the same focused or unfocused
— on that version focus barely changes the edit-mode update rate, so this
is a general speed-up rather than an unfocused-only fix. Interval 0 and 16
measure identically there, so the default keeps the 16ms (~60Hz) throttle
to cap the forced rate on setups whose loop can run faster.

Implements the auto-activation half of #173; the explicit wire command and
its protocol bump are not included.
… time

The accept loops had no retry of their own: the Unix loop ends permanently
on any unexpected AcceptAsync failure, and both loops give up if the very
first listener acquire fails. The registry heartbeat kept publishing the
instance regardless, so the CLI would resolve a live target and then fail
to connect to it, over and over.

BridgeHost now clears listener readiness when an accept loop ends and runs
ListenerWatchdogPolicy on every editor tick. The policy is Unity-free and
unit-tested: it skips while a bind is in flight or the editor is
compiling/updating (domain-reload teardown is not a fault), rebinds in
place up to 5 times at 5s intervals, and on exhaustion tells the host to
unregister so the instance stops advertising as reachable. Recovery
re-publishes the registry entry and token sidecar, deleting the previous
sidecar if the rebind settled on a different hash.

The auth check — the bridge's only authentication gate — moves off
string.Equals, whose length test and early exit are observable to other
local processes, onto a fixed-time compare. Hand-rolled rather than
CryptographicOperations.FixedTimeEquals so the shared protocol source keeps
compiling on both Unity's runtime profile and .NET 9. Accept/reject
behavior is unchanged.

Closes #174
Closes #175
CHANGELOG (both copies), CLAUDE.md architecture/conventions, README
conventions, and the operator skill in both its maintainer and shipped
copies.
@yhc509
yhc509 merged commit 4c05707 into dev Aug 14, 2026
3 checks passed
@yhc509
yhc509 deleted the feat/bridge-hardening-autotick branch August 14, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant