---
phase: 06.2-gap-closure-d-50-d-53-uat-2026-05-13
plan: 02
subsystem: client/render + client/scenes
tags: [diagnostic, spike, D-41, player-renderer, local-anim, telemetry]
requirements:
  - REQ-CLI-04
  - REQ-CLI-08
gap_closure: true
dependency-graph:
  requires: []
  provides:
    - "window.__rebno.localTickRate (ticks/sec, refreshed every ~1 s)"
    - "window.__rebno.localTickVx (last vx passed to onSimulationTickLocal)"
    - "window.__rebno.localTickVy (last vy passed to onSimulationTickLocal)"
    - "window.__rebno.localTickX  (last x  passed to onSimulationTickLocal)"
    - "window.__rebno.localTickY  (last y  passed to onSimulationTickLocal)"
  affects:
    - "06.2-06 D-41 fix path selection (hypothesis-1 vs hypothesis-2)"
tech-stack:
  added: []
  patterns:
    - "1-second rolling-window counter for runtime rate sampling"
    - "spread-merge convention for window.__rebno multi-writer surface"
key-files:
  created:
    - .planning/phases/06.2-gap-closure-d-50-d-53-uat-2026-05-13/06.2-02-SUMMARY.md
  modified:
    - apps/client/src/render/PlayerRenderer.ts
    - apps/client/src/scenes/GameScene.ts
decisions:
  - "Telemetry is unconditional (no DEV-gate). Already-established precedent: existing __rebno writes were ungated for staging UAT (06-15 deploy-mode-aware fix). Keeps the diagnostic surface live in production builds where the bug actually reproduces."
  - "Tick rate is sampled as a 1-second rolling window (count/sec), not an instantaneous EMA. Simpler to reason about in DevTools — value reads as 'ticks per second' directly with no smoothing artifacts."
  - "Publication of localTickRate placed BEFORE the existing __rebno block in GameScene.update() so both telemetry surfaces commit to window in the same render frame, avoiding a torn read where Playwright/DevTools could see one without the other."
  - "Field names mirror function parameter names (vx, vy, x, y → localTickVx, localTickVy, localTickX, localTickY) so 06.2-06 can read them without ambiguity about meaning or coordinate space."
metrics:
  duration: "~12 min"
  tasks_completed: 2
  files_modified: 2
  commits:
    - 79fff03  # Task 1 — PlayerRenderer.localTickVx/Vy/X/Y
    - 5fada11  # Task 2 — GameScene.localTickRate counter
completed: 2026-05-13
---

# Phase 06.2 Plan 02: D-41 Diagnostic Telemetry Spike — Summary

**One-liner:** Added two unconditional `window.__rebno` telemetry fields — `localTickRate` (ticks/sec rolling counter) and `localTickV{x,y}/X/Y` (last sim-tick args) — to let 06.2-06 pick the right D-41 fix branch from staging UAT data instead of guessing.

## What Was Built

### Task 1 (commit 79fff03)
**File:** `apps/client/src/render/PlayerRenderer.ts`
**Change:** Extended the existing unconditional `__rebno` spread-merge inside `onSimulationTickLocal` to also write the four runtime sim-tick inputs:

```ts
g.__rebno = {
  ...(g.__rebno ?? {}),
  localFrame: state.frameKey,
  localTickVx: vx,     // NEW
  localTickVy: vy,     // NEW
  localTickX:  x,      // NEW
  localTickY:  y,      // NEW
};
```

Tag extended to `[impl->REQ-CLI-04] [impl->REQ-CLI-08]` with a comment pointer back to this plan.

### Task 2 (commit 5fada11)
**File:** `apps/client/src/scenes/GameScene.ts`
**Change:** Added a 1-second rolling-window counter that publishes `window.__rebno.localTickRate`:

1. Two new private fields on `GameScene` (declared next to `simTickAccumulator`):
   - `private localTickCounter = 0;`
   - `private localTickWindowStart = 0;`

2. Counter increment inside the `simTickAccumulator >= SIM_TICK_MS` while-loop, immediately after the `onSimulationTickLocal(...)` call.

3. Publication block in `update()` placed **before** the existing `__rebno` write — uses the same spread-merge convention. When `_time - localTickWindowStart >= 1000`, it writes `localTickRate = localTickCounter`, then zeros the counter and rolls the window forward.

Both edits are tagged `[impl->REQ-CLI-04] [impl->REQ-CLI-08] D-41 cycle-3 diagnostic — see 06.2-02-PLAN.md`.

## Verification

| Check | Result |
| --- | --- |
| `pnpm --filter @rebno/client typecheck` | exit 0 |
| `grep "localTickV[xy]\|localTickX\|localTickY"` in PlayerRenderer.ts | 4 hits (208–211) |
| `grep "localTickRate\|localTickCounter\|localTickWindowStart"` in GameScene.ts | 7 hits |
| `[impl->REQ-CLI-04]` tags in both modified files | present |
| Existing render / sim-tick behavior changed | **no** — purely additive telemetry |

## How To Use The Telemetry (NOTE TO 06.2-06 EXECUTOR)

Deploy this branch to staging, log in as a normal user, then in Chrome DevTools console:

```js
// Hold KeyD for ~1 s, then run:
window.__rebno
// → { …, localTickRate: <N>, localTickVx: <vx>, localTickVy: <vy>, localTickX, localTickY, localFrame: '<key>' }
```

### Decision matrix for 06.2-06 fix path selection

| `localTickRate` (after 1 s hold) | `localTickVx` during hold | Diagnosis | Fix path in 06.2-06 |
| --- | --- | --- | --- |
| **0** | (any) | **Hypothesis-1 confirmed.** `predictTick` is bailing or the GameScene callsite gate is broken — the while-loop body never executes. | Fix the callsite gate / prediction-engine bail condition in GameScene.update() so onSimulationTickLocal fires at ~30 Hz under input. |
| **≈30** | **0** | **Hypothesis-2 confirmed.** Sim-tick fires correctly, but `predictTick` is returning post-step-zero velocity. This is the **BNO instant-set behavior** — the BNO movement model writes `curspeed * dir_x` into `x` (instant teleport per tick) and then leaves `vx_post = 0` because there's no inertia carry. `deriveFrame(0, 0, …)` then collapses to the Stand frame. | In GameScene.update() while-loop, pass `axis_x_held * RUN_SPEED_PX_PER_TICK` (and same for y) as the **velocity** argument to `onSimulationTickLocal`, NOT `localSim.vx/vy` from `predictTick`. The held-axis intent is the correct anim driver, not the post-step velocity. (Same pattern already used for remote players on line ~731-732.) |
| **≈30** | **non-zero** | Both invariants hold but sprite still doesn't animate. New hypothesis-3 required — investigate `cyclePhase` advance, `tickAccumulator`, or atlas frame-key resolution. | Re-scope 06.2-06; instrument deriveFrame internals. |
| **0** | non-zero | Inconsistent — impossible if telemetry is wired correctly. | Re-check that 06.2-02 actually deployed; clear bfcache and reload. |

The 06.2-06 plan was authored against this decision matrix — its branching logic should map 1:1 to the rows above.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Worktree missing node_modules and internal package builds**
- **Found during:** Task 1 verification (`pnpm --filter @rebno/client typecheck` failed with `Cannot find type definition file for 'node'`).
- **Issue:** Fresh worktree had no installed dependencies; client typecheck depends on `@rebno/game-logic` and `@rebno/protocol` dist outputs.
- **Fix:** Ran `pnpm install --prefer-offline` then built `@rebno/game-logic` and `@rebno/protocol`. After this, client typecheck passed cleanly for both tasks.
- **Files modified:** None (build artifacts only — outside scope).
- **Commit:** N/A (no source change).

**2. [Process — not a deviation, but recorded] Initial Edit landed on main repo, not worktree**
- **Found during:** Task 1 commit prep (`git status` in worktree showed PlayerRenderer.ts unchanged after Edit).
- **Issue:** First Edit call used the absolute path to the main repo (`C:\Users\decid\Documents\projects\rebno\apps\client\...`), which the file-system resolves to the **main** working tree, NOT the worktree at `C:\Users\decid\Documents\projects\rebno\.claude\worktrees\agent-aa70707bf788662a4\...`. This is the documented #3099 absolute-path safety pitfall.
- **Fix:** Reverted the unintended main-repo change via `git checkout --` and re-applied the edit using the worktree-absolute path. Verified the edit landed inside the worktree before committing.
- **Files modified:** None of the wrong write persisted.
- **Commit:** N/A.

**3. [Whitespace noise]** `pnpm --filter @rebno/protocol build`'s prebuild sync-script touched `packages/protocol/src/legacy-opcodes.ts` line-endings (LF→CRLF on Windows). Reverted via `git checkout --` before committing Task 1 to keep the commit scope clean. No source-content change.

No Rule 1 / Rule 2 / Rule 4 deviations encountered.

## Auth Gates

None.

## Known Stubs

None — the diagnostic is fully wired and writes real runtime values.

## Threat Flags

None — telemetry writes to `window.__rebno` (a non-secret diagnostic surface that already exists in production) and the new fields are local-only sim-tick values (velocity components, tile-space coordinates, integer counter). No new network endpoint, auth path, file access, or schema-boundary surface introduced.

## Self-Check

**Files claimed created/modified:**
- `apps/client/src/render/PlayerRenderer.ts` — FOUND (modified, 4 new telemetry fields at lines 208–211)
- `apps/client/src/scenes/GameScene.ts` — FOUND (modified, 7 references to new fields)
- `.planning/phases/06.2-gap-closure-d-50-d-53-uat-2026-05-13/06.2-02-SUMMARY.md` — being written now

**Commits claimed:**
- `79fff03` — FOUND on `worktree-agent-aa70707bf788662a4` (`git log --oneline`)
- `5fada11` — FOUND on `worktree-agent-aa70707bf788662a4` (`git log --oneline`)

## Self-Check: PASSED
