LearnNewsExamplesServices
Frontmatter
id12931
titleNeural Link: observe_motion needs node-granularity (cells are invisible to motion forensics)
stateClosed
labels
enhancementaimodel-experience
assigneesneo-gpt
createdAtJun 12, 2026, 1:50 AM
updatedAtJun 12, 2026, 9:44 PM
githubUrlhttps://github.com/neomjs/neo/issues/12931
authorneo-fable-clio
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 12, 2026, 9:44 PM

Neural Link: observe_motion needs node-granularity (cells are invisible to motion forensics)

Closed v13.1.0/archive-v13-1-0-chunk-1 enhancementaimodel-experience
neo-fable-clio
neo-fable-clio commented on Jun 12, 2026, 1:50 AM

Context

Born from live friction during tonight's devindex grid-corruption debugging (#12930 / #12929 family). The operator's direct challenge: "do we need to enhance NL, to make it more 'visual' for models?" This ticket is the evidence-grounded answer: for the defect class we just hit, models don't need pixels — they need rendered-geometry truth at the granularity where the defect lives, and NL is exactly one parameter-type short of providing it.

Release classification: boardless — agent-tooling enhancement, post-release.

The Problem

observe_motion samples client rects over a time window — the designated render-layer instrument (operational handbook §4.1 pairs it with get_drag_trace's logic layer). But it accepts componentIds only. Grid cells are pooled raw vdom nodes, NOT components (verify_component_consistency on any grid row shows items: 0) — so cell motion is structurally invisible to NL's motion forensics.

Tonight's two operator repros (#12930 comment IC_kwDODSospM8AAAABF0-9FA) live precisely in that blind spot:

  • Repro A: drag reversal → header (a component — observable) returns, cells (nodes — invisible) don't.
  • Repro B: overdrag scroll → no cells animate while header switching proceeds.

An agent with node-granularity rect sampling would have seen both DIRECTLY in one call: cell rects diverging from their header's rect (A), cell rects frozen during overdrag (B). Instead the agent (me) had to infer the defect from structural state (the id-generation doubling) and rely on the operator's eyes for the motion symptom. The handbook's own architecture is right; the instrument's reach is one level too shallow.

The Architectural Reality

  • observe_motion resolves componentIds → DOM ids and samples getBoundingClientRect per tick on the main thread. getBoundingClientRect is post-transform rendered geometry — for layout/transform/animation defects it IS the visual truth; pixels add nothing for this class.
  • Cells already have stable DOM ids (<rowId>__cell-N / <rowId>__<field> — both schemes, per #12930). The sampler needs only to accept raw element ids; the id→element resolution is the entire delta.
  • The genuinely-pixel tier (paint-level defects: z-order, clipping, opacity, GPU-layer artifacts — where geometry can lie) stays out of scope: that's screenshot territory, served today by browser-MCP/Playwright outside NL. Keeping NL geometry-true and delegating paint-truth is the right boundary.

The Fix

  1. observe_motion accepts nodeIds: string[] (raw DOM element ids) alongside componentIds — same sampler, same trace shape, per-id resolution fallback Neo.getComponent(id) || document.getElementById(id) on the main-thread side.
  2. Optional convenience: cellsOf: {rowId} expanding to that row's current cell nodes (saves the agent a consistency-call round trip).
  3. Doc delta: handbook §4.1 gains the node-granularity guidance + the "geometry-truth vs paint-truth" boundary note.
  4. (Follow-up candidate, separate ticket if wanted: declarative motion assertions for /whitebox-e2e — "these node rects must return to baseline after reversal" — turning tonight's repro A into a permanent regression test.)

Contract Ledger Matrix

(added 2026-06-12 per peer intake feedback — the tool-surface change needs row-level anchors before implementation)

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
observe_motion MCP tool schema (EXISTING, modified) ai/mcp/server/neural-link/openapi.yaml:656-678 (/component/observeMotion, required: [componentIds]) required relaxes to "at least one of componentIds / nodeIds"; new nodeIds: string[] property (raw DOM element ids); optional cellsOf: {rowId} convenience expansion Calls passing only componentIds behave exactly as today (additive change) Tool description block in the same yaml Schema read 2026-06-12; x-pass-as-object: true confirms object-shaped args
Tool routing (EXISTING, unchanged) ai/mcp/server/neural-link/toolService.mjs:48 No change — same binding to InteractionService.observeMotion n/a n/a Routing line verified
Service relay (EXISTING, modified) ai/services/neural-link/InteractionService.mjs:66 (observeMotion({componentIds, durationMs, intervalMs, sessionId})) Adds nodeIds / cellsOf pass-through to the App-worker client Absent params pass undefined — current callers unaffected JSDoc on the method Signature verified
Runtime sampler (EXISTING, modified) src/ai/client/ComponentService.mjs:75-87 — validates componentIds non-empty, resolves Neo.getComponent(componentIds[0]) (component-only: THE gap), clamps durationMs 100–8000 / intervalMs 16–1000 Validation becomes "componentIds or nodeIds non-empty"; per-id resolution falls back component-registry → raw DOM id; trace shape unchanged ({componentIds, samples: [{t, rects}]} gains the node ids in its rect keys); clamps unchanged Component-only calls hit the identical code path Method JSDoc Implementation read 2026-06-12; the empty-array throw at :76-78 is the surface the new validation replaces
Operational handbook §4.1 (EXISTING, modified) .agents/skills/neural-link/references/operational-handbook.md Gains node-granularity guidance + the geometry-truth vs paint-truth boundary note n/a (is the docs row) Handbook section confirmed as the instrument-pairing source

Evolution note (2026-06-12): Contract Ledger added in place by the ticket author after peer intake review yielded the ticket citing its absence; the Fix section's "main-thread side" resolution phrasing is superseded by the sampler row above (resolution lives in the App-worker client, raw-id rect reads relay to Main) — the implementing PR settles internals within that contract.

Acceptance Criteria

  • observe_motion({nodeIds: [...]}) returns rect traces for non-component vdom nodes (grid cells as the test case).
  • Mixed calls (componentIds + nodeIds) work in one window.
  • Handbook §4.1 updated (node granularity + the pixels-boundary note).
  • Verified against #12930's Repro A: one call captures the header-returns/cells-don't divergence.

Out of Scope

  • Screenshot/paint-truth capture inside NL (browser-MCP territory; revisit only if a geometry-lies defect class materializes).
  • The motion-assertion DSL for whitebox-e2e (follow-up, separate scope).
  • The #12930 fix itself (this instrument is its verification dependency, not its solution).

Related

  • #12930 (primary consumer — bisect verification + Repro-A falsifier), #12929, #12883 (the defect family that exposed the gap), #12884 (sibling NL gap, different surface).
  • Operational handbook: .agents/skills/neural-link/references/operational-handbook.md §4.1.

Live latest-open sweep: checked latest 15 open issues at 2026-06-11T23:49Z; no equivalent found (#12884 = window-registration gap, different NL surface). A2A in-flight sweep: clean (this turn's traffic only).

Lane note: filed unassigned — it's claimable by anyone; I'll claim it when #12930's bisect reaches verification stage if it's still open, since Repro A's falsifier needs it.

Origin Session ID: e6b095bb-4d88-4aeb-a0ec-7d47e0f8f7ce

Retrieval Hint: "observe_motion node granularity cells invisible motion forensics neural link visual"

tobiu referenced in commit 1931919 - "feat(neural-link): sample raw nodes in observe_motion (#12931) (#12993)" on Jun 12, 2026, 9:44 PM
tobiu closed this issue on Jun 12, 2026, 9:44 PM