Skip to content

Observe director targets by their built ids and forward machine wiring through the timeout wrappers - #65

Merged
Enzx merged 2 commits into
mainfrom
fix/wrapper-and-director-observer-wiring
Jul 30, 2026
Merged

Enzx merged 2 commits into
mainfrom
fix/wrapper-and-director-observer-wiring

Conversation

@Enzx

@Enzx Enzx commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Two wiring gaps, both found by a downstream consumer that observes runs and wraps steps in timeouts.

What was broken

1. Observers saw a nameless id for the node a dispatch or plain start jumps to. Directors — the event-entry dispatcher (StartWithEvents), the plain-run Otherwise route, ChoiceState/SwitchState, and any user director — hold target NodeIds captured at authoring time, before Build() applies display names. The run loops routed on those raw ids, so OnTransition/OnStateEntered/OnStateExited for the first node of every entry chain carried an empty Name (a single-step chain never surfaced a named event at all), while every ordinary edge hop reported the built, named id. Any observer that keys on NodeId.Name — progress publishers, trace tooling — silently dropped the first authored step of every chain.

2. The timeout wrappers swallowed machine wiring. AsyncTimeoutState/TimeoutState implemented only the logic interface, so the blackboard stamping walk, agent stamping, and the machines' log-report resolution all stopped at the wrapper: a blackboard-using state inside a timeout lost its board context, its Log calls went nowhere, and a graph whose only agent acceptor sat inside a timeout made SetAgent throw "no nodes implement IAgentSettable".

What changed

  • All four run loops (sync/async FSM, sync/async token) canonicalize a director-selected id against the graph before routing, so observers see the built id exactly as they do for edge destinations. Identity is the index, so routing is unchanged; an unknown index still fails with the existing node-not-found error.
  • Both timeout wrappers forward the stamped blackboard context to the wrapped state (IBlackboardSettable, forward-only — the wrapper holds no board state, so nothing is duplicated).
  • A new ILogicWrapper seam (WrappedLogic) exposes the wrapped instance for the wiring that cannot be forwarded interface-to-interface: agent stamping is generic in the agent type, and the sync report slot lives on the State base class. The graph's agent walk and all four machines' report tables resolve through decorator layers, so the wrapped state's own slots are wired and cleared exactly as if it were bare. A decorated composite still surfaces its children to the agent walk.
  • Public API baselines record the new surface (ILogicWrapper, the wrappers' interface lists).

Other wrappers checked

SyncLogicAdapter was already unwrapped at the LogicNode level; AllState/AsyncAllState hold delegates that receive the routed context as a parameter; the machine-wrapping composites already implement IBlackboardSettable/ISubGraphProvider. The two timeout states were the only decorators with the hole.

Resume semantics

Untouched. Resume/ResumeDeep already restore the current node from the graph's own node table (built ids), and restore deliberately replays no observer events — both fixes only affect the live routing/wiring paths.

Tests

  • DirectorTargetObservationTests (9): entered/exited/transition ids carry the built name for an event dispatch target (full-run and stepped), the plain-start Otherwise target, a single-step entry chain, and choice branch targets — sync and async; plus a single-node-graph pin. All fail before the fix.
  • TimeoutWiringForwardingTests (7): board, log report, and agent reach a state inside the async and sync timeout wrappers, including the sync-state-under-async-machine bridge case. All fail before the fix.
  • Full suite green: 721 + 203 (was 705 + 203 at baseline).

Downstream note

The Aitomata consumer (project reference against this checkout) was smoke-checked with its full acceptance harness after both fixes: everything green — 222 backend tests plus the frontend phase, no test there asserted the old missing-first-step behavior. Its run-progress publisher now correctly emits step-started/step-finished for the first authored step of every chain, and its StepTimeoutState forwarding-subclass workaround (which forwards IBlackboardSettable/ILogReporter itself) keeps working unchanged and can be retired at the consumer's leisure.

Enzx added 2 commits July 30, 2026 03:16
Directors (event dispatch, the plain-run Otherwise chain, choice/switch)
hold target NodeIds captured at authoring time, before Build() applies
display names. The run loops routed on those raw ids, so observers saw a
nameless id for the node a dispatch or a plain start jumps to - the first
node of every entry chain emitted no name-keyed step events, and a
single-step chain emitted none - while ordinary edge destinations (renamed
at Build()) observed correctly.

The four run loops now canonicalize a director-selected id against the
graph before routing, so OnTransition/OnStateEntered/OnStateExited carry
the built id exactly as they do for edge hops. Identity is the index, so
routing is unchanged; unknown indexes still fail with the existing
node-not-found error.
AsyncTimeoutState and TimeoutState hid the state they wrap from all three
machine wiring channels: the blackboard stamping walk, agent stamping, and
the log-report tables the machines resolve at construction. A
blackboard-using state inside a timeout silently lost its board context,
its log reports went nowhere, and a graph whose only agent acceptor sat
inside a timeout made SetAgent throw "no nodes implement IAgentSettable".

Both wrappers now forward the stamped blackboard context to the wrapped
state (IBlackboardSettable, forward-only - the wrapper holds no board
state), and a new ILogicWrapper seam exposes the wrapped instance for the
wiring that cannot be forwarded interface-to-interface: agent stamping
(generic in the agent type) reaches through decorator layers in the graph
walk, and all four machines' report tables resolve the wrapped state's own
reporter so log wiring and slot clearing behave exactly as for a bare
state. Timeout semantics are untouched; the public API baselines record
the new surface.
@Enzx
Enzx merged commit 6ee3c24 into main Jul 30, 2026
3 checks passed
@Enzx
Enzx deleted the fix/wrapper-and-director-observer-wiring branch July 30, 2026 22:25
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