Compare commits

...

1 Commits

Author SHA1 Message Date
vh a35ed7af19 docs(diagnostics): R30 gap-injection harness — banked (run closed on offline+face-validity)
Deployed gap-injection run closed by operator steer 2026-07-04: R30 graduates on
offline-tests + human face-validity, no deployed harness build, no endpoint.
Harness (read/predict/record; write side stubbed) + brokkr's R30.10 protocol pin
(de8357f) banked as drop-in for the parked powered true-tau perceptual study.
predict() self-validated against brokkr's N=0 anchors (1h/10h/1wk).
2026-07-03 23:14:48 -07:00
2 changed files with 174 additions and 0 deletions
@@ -0,0 +1,64 @@
# R30 gap-injection — deployed-run harness (BANKED, not run)
**Status:** CLOSED — not executed. On 2026-07-04 the operator steered **close R30 on
offline-tests + human face-validity**, no deployed harness build, no endpoint. No
`(agent, N, Δt, observed p/a/d)` series was collected. This harness + brokkr's protocol
are **banked / drop-in** for the parked powered true-τ perceptual study if it is ever
commissioned.
**Spec pin:** brokkr R30.10 protocol `de8357f`
`brokkr-smithy/research/R30-ocean-derived-mood-dynamics/empirics/r30-gap-injection-protocol.md`.
## Why it was closed (not built)
The deployed gap-injection run was **confirmatory, not measuring** (brokkr's §0 reframe:
against a deployed system the fade is `exp(−Δt/τ_shipped)` by construction, so a fit
returns τ_shipped tautologically — the run graduates the interim coefficients, it does not
measure them). Given that:
- The repo's **offline tests already cover the OU formula + BOTH directions**
(`high_N_fades_slower_than_low_N` decay, `phenotype_high_n_bigger_negative_excursion` gain).
- **b17 is deployed-clean** on demo + personal.
- The ①-approved **mood-holds-within-conversation** IS the face-validity call.
…the interim coefficients **graduate validated-as-shipped** with no deployed run needed.
The build cost that would have been required (and was declined):
- No deployed write affordance exists; the in-memory `_user_moods` cache **shadows** raw
`persona_mood.db` writes, so a clean inject needs an in-service **set-mood/back-date
endpoint** (set p/a/d + `updated_at` + evict cache).
- `moody-lofn N=+0.8` **does not exist** (only lofn `N=0.5`; tier-3 empty OCEAN → N=0), so
the gain half would have needed a new high-N tier-1 agent + deploy — dropped as the
expensive, offline-redundant half.
## What is validated (harness self-check)
`harness.py::predict()` reproduces brokkr's stated N=0 P-axis retention anchors **exactly**:
| Δt | predicted | brokkr stated |
|---|---|---|
| 1h (3600s) | 0.904837 | 0.905 |
| 10h/overnight (36000s) | 0.367879 | 0.368 |
| 1 week (604800s) | 5.06e-08 | 5e-08 |
Per-axis τ confirms arousal fades ~1.9× faster (N=0: τ_P = τ_D = 10h, τ_A = 5.26h).
## Personal `:8081` b17 baseline survey (candidate N-grid agents, all at rest)
| agent | baseline p/a/d | note |
|---|---|---|
| lofn | 0.809 / 0.153 / 0.248 | production, N=0.5 |
| mask | 0.0 / 0.0 / 0.0 | zero baseline → zero-N candidate |
| forseti | 0.239 / 0.696 / 0.095 | production |
| mimir | 0.615 / 0.438 / 0.304 | production |
(sindra 404s — owner-scoped tier-3, expected.)
## To un-bank (if the powered study is commissioned)
1. Worldtree builds the in-service **set-mood/back-date endpoint** (set p/a/d + `updated_at`
+ evict `_user_moods`), or the read-only fade-preview variant if that satisfies D1.
2. Wire `harness.py::freeze_start()` + `backdate()` to that endpoint (near-zero rebuild).
3. Run the 3×8 grid (frozen start p=0.6/a=+0.5/d=0.3 × Δt grid), hand brokkr the
`(agent, N, Δt, observed p/a/d)` table + per-agent `baseline_pad()`; he runs D1/D2/D3.
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""R30 gap-injection probe harness (ratatoskr instrument).
Throwaway probe per the R30.10 protocol
brokkr-smithy/research/R30-ocean-derived-mood-dynamics/empirics/r30-gap-injection-protocol.md (de8357f)
Deliverable: the (agent, N, Δt, observed p/a/d) table + per-agent baseline_pad + the
frozen start vector, handed to brokkr who runs D1/D2/D3 graduation. This harness does NOT
own the verdict; the predicted/residual columns are a sanity aid only.
READ + PREDICT + RECORD are fixed by the spec. freeze_start() + backdate() are the WRITE
side and are STUBBED pending worldtree's mechanism on personal :8081 (direct persona_mood
DB write vs a test affordance). Data lands on a diag/ branch; NOT ratatoskr production code.
"""
import json
import math
import os
import urllib.request
import urllib.error
BASE = os.environ.get("WORLDTREE_API_URL", "http://10.250.50.152:8081")
KEY = os.environ["WORLDTREE_API_KEY"]
# --- R30.10 spec constants (FIXED) ---
TAU_BASE_S = 36000.0 # τ_base = 10h
BETA_N = 0.25 # τ_P = τ_base · exp(β_N · N)
R_A = 1.9 # τ_A = τ_P / r_A ; τ_D = τ_P
FROZEN_START = {"pleasure": -0.60, "arousal": +0.50, "dominance": -0.30}
DT_GRID = [0, 60, 600, 3600, 14400, 36000, 86400, 604800] # s
AXES = ("pleasure", "arousal", "dominance")
EPS = 1e-3 # D1 tolerance (PAD units)
def tau(axis, N):
tau_p = TAU_BASE_S * math.exp(BETA_N * N)
return {"pleasure": tau_p, "arousal": tau_p / R_A, "dominance": tau_p}[axis]
def retention(axis, dt, N):
"""ρ_k(Δt,N) = exp(−Δt / τ_k(N)) — predicted deviation-retention fraction."""
return math.exp(-dt / tau(axis, N))
def _get(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": f"Bearer {KEY}"})
with urllib.request.urlopen(req, timeout=8) as r:
return json.load(r)
def read_pad(agent):
"""get_state read: returns (pad, baseline_pad). pad == peek_relaxed(now), OU-faded, non-mutating."""
d = _get(f"/agents/{agent}/persona_state")
return d["pad"], d["baseline_pad"]
# --- WRITE SIDE: STUBBED pending worldtree mechanism (msg 01KWNVHTFP…) ---
def freeze_start(agent, pad):
"""Set persona_mood pad directly to the displaced vector (no live appraisal)."""
raise NotImplementedError("worldtree mechanism pending: freeze displaced start mood pad")
def backdate(agent, dt_s):
"""Set persona_mood.updated_at = now dt_s so peek_relaxed fades by exactly Δt."""
raise NotImplementedError("worldtree mechanism pending: back-date persisted updated_at")
def predict_selfcheck():
"""Validate retention() against brokkr's stated N=0 P-axis anchors (spec §2 D3)."""
print("predict() self-check — N=0 P-axis retention vs brokkr's stated anchors:")
expect = {"1h": (3600, 0.905), "10h(overnight)": (36000, 0.368), "1wk": (604800, 5e-8)}
ok = True
for label, (dt, want) in expect.items():
got = retention("pleasure", dt, 0.0)
match = abs(got - want) < (want * 0.01 + 1e-9) # 1% or float-floor
ok = ok and match
print(f" {label:>16}: got {got:.6g} expect {want:.6g} {'OK' if match else 'MISMATCH'}")
# per-axis τ at a couple N points (informational)
print("τ (hours) by axis × N:")
for N in (-0.6, 0.0, 0.8):
taus = {a: tau(a, N) / 3600 for a in AXES}
print(f" N={N:+.1f}: P={taus['pleasure']:.2f}h A={taus['arousal']:.2f}h D={taus['dominance']:.2f}h")
return ok
def run_cell(agent, N, dt, baseline):
"""One (agent, Δt) cell: freeze start → back-date by Δt → read faded pad → record row.
Blocked until freeze_start()/backdate() are wired to worldtree's mechanism."""
freeze_start(agent, FROZEN_START)
backdate(agent, dt)
pad, _ = read_pad(agent)
row = {"agent": agent, "N": N, "dt_s": dt}
for k in AXES:
obs = pad[k]
dev0 = abs(FROZEN_START[k] - baseline[k])
pred_dev = dev0 * retention(k, dt, N)
obs_dev = abs(obs - baseline[k])
row[f"obs_{k[0]}"] = obs
row[f"pred_dev_{k[0]}"] = pred_dev
row[f"resid_{k[0]}"] = obs_dev - pred_dev
return row
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "read":
agent = sys.argv[2]
pad, base = read_pad(agent)
print(json.dumps({"agent": agent, "pad": pad, "baseline_pad": base}, indent=2))
else:
predict_selfcheck()