Sync from GitHub: 500d328a89ae2aee6e3c4971d12c2c008ae336c4
Browse files- app.py +14 -145
- nuwave/organism.py +27 -0
app.py
CHANGED
|
@@ -1243,150 +1243,20 @@ def on_interleaved_benchmark(
|
|
| 1243 |
nw_msgs.append({"role": "assistant", "content": resp_nw})
|
| 1244 |
nw_organism.record_outcome(prompt_text, resp_nw, success=True)
|
| 1245 |
|
| 1246 |
-
#
|
| 1247 |
-
#
|
| 1248 |
-
#
|
| 1249 |
-
#
|
| 1250 |
-
#
|
| 1251 |
-
#
|
| 1252 |
-
#
|
| 1253 |
-
#
|
| 1254 |
-
#
|
| 1255 |
-
#
|
| 1256 |
-
#
|
| 1257 |
-
#
|
| 1258 |
-
#
|
| 1259 |
-
#
|
| 1260 |
-
# 3. Collective tokens-saved (scoped to entire pith set) —
|
| 1261 |
-
# did the surfacing as a whole help compress the prompt?
|
| 1262 |
-
#
|
| 1263 |
-
# Stimulate-then-step before reward is the canonical inject_reward
|
| 1264 |
-
# pattern. surface_extract used write_mode=False, leaving no
|
| 1265 |
-
# eligibility traces; we re-fire the pith nodes here briefly so
|
| 1266 |
-
# traces form, then scoped reward modulates them.
|
| 1267 |
-
# Phase 2 (revised 2026-04-27 after Runs 19-20 showed actively-
|
| 1268 |
-
# harmful behavior). Two bugs in the prior implementation, fixed
|
| 1269 |
-
# here:
|
| 1270 |
-
#
|
| 1271 |
-
# Bug 1 (DROPPED): Channel 3 (collective tokens-saved penalty)
|
| 1272 |
-
# applied uniformly to all pith nodes. When NuWave used more
|
| 1273 |
-
# tokens than baseline, it depressed legitimate retrieval
|
| 1274 |
-
# pathways (high-prior-weight question nodes) faster than
|
| 1275 |
-
# marginal noise (low-prior-weight narrative nodes), inverting
|
| 1276 |
-
# the intended effect. Tokens-saved is a turn-level signal
|
| 1277 |
-
# with no per-node attribution — feeding it as a scoped
|
| 1278 |
-
# penalty was a wrong signal-to-scope binding. Removed.
|
| 1279 |
-
#
|
| 1280 |
-
# Bug 2 (FIXED): stimulate(current=1.5) + step() left residual
|
| 1281 |
-
# voltage on stimulated nodes that bled into the next turn's
|
| 1282 |
-
# surface_extract, biasing retrieval toward the very nodes
|
| 1283 |
-
# Phase 2 was trying to selectively reward. Replaced with
|
| 1284 |
-
# prime_and_propagate(write_mode=True), the canonical method
|
| 1285 |
-
# for forming eligibility traces — bounded propagation,
|
| 1286 |
-
# designed for plasticity, used by Tonic's heuristic
|
| 1287 |
-
# ouroboros and by canonical surface_extract patterns.
|
| 1288 |
-
#
|
| 1289 |
-
# Telemetry is now surfaced INTO the per-turn result dict as
|
| 1290 |
-
# phase2_diag, visible in the JSON output rather than only in
|
| 1291 |
-
# runtime logs (Josh: "you set this up, that's your department").
|
| 1292 |
-
_phase2_diag = {"fired": False, "reason": None}
|
| 1293 |
-
try:
|
| 1294 |
-
if pith_ids and resp_nw and nw_organism._graph is not None:
|
| 1295 |
-
_rewards_before = getattr(
|
| 1296 |
-
nw_organism._graph, "_total_rewards_injected", 0,
|
| 1297 |
-
)
|
| 1298 |
-
|
| 1299 |
-
_resp_emb = np.asarray(
|
| 1300 |
-
nw_organism._embed_fn(resp_nw), dtype=np.float32,
|
| 1301 |
-
)
|
| 1302 |
-
_resp_n = _resp_emb / (np.linalg.norm(_resp_emb) + 1e-8)
|
| 1303 |
-
_query_emb = np.asarray(
|
| 1304 |
-
nw_organism._embed_fn(prompt_text), dtype=np.float32,
|
| 1305 |
-
)
|
| 1306 |
-
_query_n = _query_emb / (np.linalg.norm(_query_emb) + 1e-8)
|
| 1307 |
-
|
| 1308 |
-
_per_node_strengths = []
|
| 1309 |
-
|
| 1310 |
-
with nw_organism._graph_lock:
|
| 1311 |
-
# Use prime_and_propagate(write_mode=True) — the
|
| 1312 |
-
# canonical trace-forming method. Bounded propagation
|
| 1313 |
-
# means voltages don't leak into next turn, and
|
| 1314 |
-
# write_mode enables STDP plasticity from the activity.
|
| 1315 |
-
_seeds = [
|
| 1316 |
-
_pid for _pid in pith_ids
|
| 1317 |
-
if _pid in nw_organism._graph.nodes
|
| 1318 |
-
]
|
| 1319 |
-
if _seeds:
|
| 1320 |
-
_prop_result = nw_organism._graph.prime_and_propagate(
|
| 1321 |
-
node_ids=_seeds,
|
| 1322 |
-
currents=[1.0] * len(_seeds),
|
| 1323 |
-
steps=3,
|
| 1324 |
-
write_mode=True,
|
| 1325 |
-
)
|
| 1326 |
-
_fired_count = (
|
| 1327 |
-
len(_prop_result.fired_nodes)
|
| 1328 |
-
if hasattr(_prop_result, "fired_nodes")
|
| 1329 |
-
else 0
|
| 1330 |
-
)
|
| 1331 |
-
|
| 1332 |
-
# Per-node similarity rewards. Channel 3 (collective
|
| 1333 |
-
# tokens-saved) DELETED — see bug analysis above.
|
| 1334 |
-
for _pid in pith_ids:
|
| 1335 |
-
_node_emb = nw_organism._embeddings.get(_pid)
|
| 1336 |
-
if _node_emb is None:
|
| 1337 |
-
continue
|
| 1338 |
-
_node_n = _node_emb / (
|
| 1339 |
-
np.linalg.norm(_node_emb) + 1e-8
|
| 1340 |
-
)
|
| 1341 |
-
_strength_resp = max(-1.0, min(
|
| 1342 |
-
1.0,
|
| 1343 |
-
(float(np.dot(_node_n, _resp_n)) - 0.3) * 2.0,
|
| 1344 |
-
))
|
| 1345 |
-
_strength_query = max(-1.0, min(
|
| 1346 |
-
1.0,
|
| 1347 |
-
(float(np.dot(_node_n, _query_n)) - 0.3) * 2.0,
|
| 1348 |
-
))
|
| 1349 |
-
nw_organism._graph.inject_reward(
|
| 1350 |
-
strength=_strength_resp, scope={_pid},
|
| 1351 |
-
)
|
| 1352 |
-
nw_organism._graph.inject_reward(
|
| 1353 |
-
strength=_strength_query, scope={_pid},
|
| 1354 |
-
)
|
| 1355 |
-
_per_node_strengths.append({
|
| 1356 |
-
"id": _pid,
|
| 1357 |
-
"resp_sim": round(_strength_resp, 3),
|
| 1358 |
-
"query_sim": round(_strength_query, 3),
|
| 1359 |
-
})
|
| 1360 |
-
|
| 1361 |
-
# Consolidate — two step()s let STDP propagate
|
| 1362 |
-
# the rewarded eligibility traces into actual
|
| 1363 |
-
# weight changes.
|
| 1364 |
-
for _ in range(2):
|
| 1365 |
-
nw_organism._graph.step()
|
| 1366 |
-
else:
|
| 1367 |
-
_fired_count = 0
|
| 1368 |
-
|
| 1369 |
-
_rewards_after = getattr(
|
| 1370 |
-
nw_organism._graph, "_total_rewards_injected", 0,
|
| 1371 |
-
)
|
| 1372 |
-
_phase2_diag = {
|
| 1373 |
-
"fired": True,
|
| 1374 |
-
"seeds": len(_seeds) if _seeds else 0,
|
| 1375 |
-
"fired_count_post_pap": _fired_count,
|
| 1376 |
-
"rewards_injected_delta": _rewards_after - _rewards_before,
|
| 1377 |
-
"per_node_strengths": _per_node_strengths,
|
| 1378 |
-
}
|
| 1379 |
-
except Exception as exc:
|
| 1380 |
-
# Promoted from debug → warning. Surface in JSON so we don't
|
| 1381 |
-
# need runtime logs to see what's happening.
|
| 1382 |
-
logger.warning(
|
| 1383 |
-
"Phase 2 feedback loop FAILED on turn %d: %s: %s",
|
| 1384 |
-
i + 1, type(exc).__name__, exc,
|
| 1385 |
-
)
|
| 1386 |
-
_phase2_diag = {
|
| 1387 |
-
"fired": False,
|
| 1388 |
-
"reason": f"{type(exc).__name__}: {exc}",
|
| 1389 |
-
}
|
| 1390 |
|
| 1391 |
# Drain the concept queue before the next turn — makes tree
|
| 1392 |
# extraction synchronous for benchmark reproducibility. Without
|
|
@@ -1458,7 +1328,6 @@ def on_interleaved_benchmark(
|
|
| 1458 |
"ignition_size": len(ignition_sets[i]),
|
| 1459 |
"pith_ids": list(pith_ids),
|
| 1460 |
"surfaced_context": _surfaced_context,
|
| 1461 |
-
"phase2_diag": _phase2_diag,
|
| 1462 |
"trees": trees_for_turn,
|
| 1463 |
"raw_extractor_output": raw_output,
|
| 1464 |
"extractor_elapsed_s": extractor_elapsed,
|
|
|
|
| 1243 |
nw_msgs.append({"role": "assistant", "content": resp_nw})
|
| 1244 |
nw_organism.record_outcome(prompt_text, resp_nw, success=True)
|
| 1245 |
|
| 1246 |
+
# Phase 2 (scoped multi-channel substrate feedback) was attempted
|
| 1247 |
+
# in commits 468fd09, ab0fdd3, e4dd297 then removed 2026-04-27.
|
| 1248 |
+
# The architectural idea (substrate-feedback-via-inject_reward) is
|
| 1249 |
+
# canonical Substrate Authority Pattern and remains correct, but
|
| 1250 |
+
# all three implementations had bugs that made them either no-op
|
| 1251 |
+
# or actively harmful: stimulate residual voltage created positive
|
| 1252 |
+
# feedback loops, Channel 3 collective penalty had wrong signal-
|
| 1253 |
+
# to-scope binding, prime_and_propagate(currents=1.0) didn't fire
|
| 1254 |
+
# seeds, and concurrent-modification races crashed 3/8 turns.
|
| 1255 |
+
# See feedback_substrate_representation_first.md — Phase 2 redesign
|
| 1256 |
+
# is deferred until representation work (discover_hyperedges hook,
|
| 1257 |
+
# type-aware retrieval scoring with expert decay) gives the
|
| 1258 |
+
# substrate the structural inductive biases that make relevance
|
| 1259 |
+
# learnable in the first place.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1260 |
|
| 1261 |
# Drain the concept queue before the next turn — makes tree
|
| 1262 |
# extraction synchronous for benchmark reproducibility. Without
|
|
|
|
| 1328 |
"ignition_size": len(ignition_sets[i]),
|
| 1329 |
"pith_ids": list(pith_ids),
|
| 1330 |
"surfaced_context": _surfaced_context,
|
|
|
|
| 1331 |
"trees": trees_for_turn,
|
| 1332 |
"raw_extractor_output": raw_output,
|
| 1333 |
"extractor_elapsed_s": extractor_elapsed,
|
nuwave/organism.py
CHANGED
|
@@ -916,6 +916,33 @@ class NuWaveOrganism:
|
|
| 916 |
self._last_active_ts = time.time()
|
| 917 |
with self._graph_lock:
|
| 918 |
step_result = self._graph.step()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 919 |
self._step_result = step_result
|
| 920 |
self._step_count += 1
|
| 921 |
|
|
|
|
| 916 |
self._last_active_ts = time.time()
|
| 917 |
with self._graph_lock:
|
| 918 |
step_result = self._graph.step()
|
| 919 |
+
|
| 920 |
+
# Hand fired nodes to the canonical hyperedge discovery
|
| 921 |
+
# primitive. discover_hyperedges() is built into NG's
|
| 922 |
+
# neuro_foundation.py but is host-driven — the substrate
|
| 923 |
+
# API is host-agnostic and doesn't auto-call discovery
|
| 924 |
+
# from inside step(). NuWave is the host for this graph,
|
| 925 |
+
# so we hand it the freshly-fired set every step. The
|
| 926 |
+
# method tracks co-fire patterns over he_discovery_window
|
| 927 |
+
# (config default 10 timesteps); creates a hyperedge when
|
| 928 |
+
# a fired pattern hits he_discovery_min_co_fires (5) and
|
| 929 |
+
# has at least he_discovery_min_nodes (3) members. All
|
| 930 |
+
# config knobs already in place from Phase 1's graph
|
| 931 |
+
# config block. Non-zero return = newly discovered HEs.
|
| 932 |
+
# Wrapped in try/except — discovery failure must not
|
| 933 |
+
# break the step() lifecycle for the rest of the organism.
|
| 934 |
+
fired_for_discovery = list(
|
| 935 |
+
getattr(step_result, "fired_node_ids", []) or []
|
| 936 |
+
)
|
| 937 |
+
if len(fired_for_discovery) >= 3:
|
| 938 |
+
try:
|
| 939 |
+
self._graph.discover_hyperedges(fired_for_discovery)
|
| 940 |
+
except Exception as exc:
|
| 941 |
+
logger.debug(
|
| 942 |
+
"discover_hyperedges raised: %s: %s",
|
| 943 |
+
type(exc).__name__, exc,
|
| 944 |
+
)
|
| 945 |
+
|
| 946 |
self._step_result = step_result
|
| 947 |
self._step_count += 1
|
| 948 |
|