Commit Β·
212a158
1
Parent(s): cf762ba
sync(neuro_foundation): v0.4.2 hibernation fix from canonical NeuroGraph
Browse filesSynced from greatnorthernfishguy-hub/NeuroGraph commit f6c576f.
Serializes delay_buffer, recent_spikes, steps_since_last_fire, and
homeostatic_steps_since_scaling so Codemine workers load warm state
rather than cold-starting on every subprocess boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- neuro_foundation.py +72 -1
- neurograph_migrate.py +25 -2
neuro_foundation.py
CHANGED
|
@@ -19,6 +19,23 @@ Design principles (PRD Β§2.1):
|
|
| 19 |
- Persistence-native: all state is serializable
|
| 20 |
|
| 21 |
# ---- Changelog ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# [2026-03-25] Claude Code (Opus 4.6) β Dynamic tuning API (SVG Phase 5)
|
| 23 |
# What: Added TUNABLE_PARAMS, update_tunable(), get_tunables() to Graph class.
|
| 24 |
# 12 SNN parameters tunable at runtime with validated bounds: learning_rate,
|
|
@@ -3632,7 +3649,7 @@ class Graph:
|
|
| 3632 |
|
| 3633 |
def _serialize_full(self) -> Dict[str, Any]:
|
| 3634 |
return {
|
| 3635 |
-
"version": "0.4.
|
| 3636 |
"timestep": self.timestep,
|
| 3637 |
"config": self.config,
|
| 3638 |
"nodes": {nid: self._serialize_node(n) for nid, n in self.nodes.items()},
|
|
@@ -3703,6 +3720,26 @@ class Graph:
|
|
| 3703 |
# Phase 2.5b: Output target learning state
|
| 3704 |
"he_last_fired_step": self._he_last_fired_step,
|
| 3705 |
"he_output_candidates": self._he_output_candidates,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3706 |
}
|
| 3707 |
|
| 3708 |
def _serialize_incremental(self) -> Dict[str, Any]:
|
|
@@ -3829,6 +3866,25 @@ class Graph:
|
|
| 3829 |
self._node_hyperedges[nid] = set()
|
| 3830 |
self._recent_spikes[nid] = deque(maxlen=20)
|
| 3831 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3832 |
# Restore synapses
|
| 3833 |
for sid, sd in data.get("synapses", {}).items():
|
| 3834 |
syn = Synapse(
|
|
@@ -4075,6 +4131,21 @@ class Graph:
|
|
| 4075 |
),
|
| 4076 |
]
|
| 4077 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4078 |
self._dirty_nodes.clear()
|
| 4079 |
self._dirty_synapses.clear()
|
| 4080 |
self._dirty_hyperedges.clear()
|
|
|
|
| 19 |
- Persistence-native: all state is serializable
|
| 20 |
|
| 21 |
# ---- Changelog ----
|
| 22 |
+
# [2026-04-14] Claude (Sonnet 4.6) β v0.4.2 Hibernation fix: serialize ephemeral process state
|
| 23 |
+
# What: Added 4 previously unserialised fields to _serialize_full()/_deserialize():
|
| 24 |
+
# _delay_buffer (in-flight spike currents), _recent_spikes (structural plasticity
|
| 25 |
+
# co-activation history), _steps_since_last_fire (zero-firing circuit breaker),
|
| 26 |
+
# HomeostaticRule._steps_since_scaling (homeostatic scaling counter).
|
| 27 |
+
# Version bumped 0.4.1 β 0.4.2.
|
| 28 |
+
# Why: Every ephemeral subprocess (CC hook, Codemine worker) loads the checkpoint
|
| 29 |
+
# cold β these fields reset to 0. With scaling_interval=25 and a single step() per
|
| 30 |
+
# call, homeostatic scaling never fired. Silent nodes never got their excitability
|
| 31 |
+
# boost. In-flight spikes were dropped. Structural plasticity lost co-activation
|
| 32 |
+
# context between calls. The substrate was effectively starting fresh each call.
|
| 33 |
+
# This is the canonical "hibernation" fix: the process believes it never stopped.
|
| 34 |
+
# How: _serialize_full() adds delay_buffer (timestepβentries), recent_spikes
|
| 35 |
+
# (nidβlist), steps_since_last_fire (int), homeostatic_steps_since_scaling (int).
|
| 36 |
+
# _deserialize() restores all four. Delay buffer validates delivery timestep > current
|
| 37 |
+
# and node existence. Homeostatic counter applied post plasticity-rules re-init.
|
| 38 |
+
# Migration v0.4.1βv0.4.2 is a no-op (old checkpoints get defaults on next save).
|
| 39 |
# [2026-03-25] Claude Code (Opus 4.6) β Dynamic tuning API (SVG Phase 5)
|
| 40 |
# What: Added TUNABLE_PARAMS, update_tunable(), get_tunables() to Graph class.
|
| 41 |
# 12 SNN parameters tunable at runtime with validated bounds: learning_rate,
|
|
|
|
| 3649 |
|
| 3650 |
def _serialize_full(self) -> Dict[str, Any]:
|
| 3651 |
return {
|
| 3652 |
+
"version": "0.4.2",
|
| 3653 |
"timestep": self.timestep,
|
| 3654 |
"config": self.config,
|
| 3655 |
"nodes": {nid: self._serialize_node(n) for nid, n in self.nodes.items()},
|
|
|
|
| 3720 |
# Phase 2.5b: Output target learning state
|
| 3721 |
"he_last_fired_step": self._he_last_fired_step,
|
| 3722 |
"he_output_candidates": self._he_output_candidates,
|
| 3723 |
+
# v0.4.2 Hibernation state β ephemeral process counters serialized so
|
| 3724 |
+
# subprocess loads (CC hook, Codemine worker) believe the process never
|
| 3725 |
+
# stopped. Without these, homeostatic scaling never fires, in-flight spikes
|
| 3726 |
+
# are dropped, structural plasticity loses co-activation context, and the
|
| 3727 |
+
# zero-firing circuit breaker loses streak continuity across calls.
|
| 3728 |
+
"delay_buffer": {
|
| 3729 |
+
str(ts): [[nid, curr] for nid, curr in entries]
|
| 3730 |
+
for ts, entries in self._delay_buffer.items()
|
| 3731 |
+
},
|
| 3732 |
+
"recent_spikes": {
|
| 3733 |
+
nid: list(spikes)
|
| 3734 |
+
for nid, spikes in self._recent_spikes.items()
|
| 3735 |
+
if spikes
|
| 3736 |
+
},
|
| 3737 |
+
"steps_since_last_fire": self._steps_since_last_fire,
|
| 3738 |
+
"homeostatic_steps_since_scaling": next(
|
| 3739 |
+
(r._steps_since_scaling for r in self._plasticity_rules
|
| 3740 |
+
if isinstance(r, HomeostaticRule)),
|
| 3741 |
+
0,
|
| 3742 |
+
),
|
| 3743 |
}
|
| 3744 |
|
| 3745 |
def _serialize_incremental(self) -> Dict[str, Any]:
|
|
|
|
| 3866 |
self._node_hyperedges[nid] = set()
|
| 3867 |
self._recent_spikes[nid] = deque(maxlen=20)
|
| 3868 |
|
| 3869 |
+
# v0.4.2: Restore recent spike history (structural plasticity co-activation)
|
| 3870 |
+
cap = self.config.get("co_activation_window", 5) * 2
|
| 3871 |
+
for nid, spike_list in data.get("recent_spikes", {}).items():
|
| 3872 |
+
if nid in self._recent_spikes and spike_list:
|
| 3873 |
+
self._recent_spikes[nid] = deque(spike_list, maxlen=cap)
|
| 3874 |
+
|
| 3875 |
+
# v0.4.2: Restore in-flight spike buffer (currents scheduled for future delivery)
|
| 3876 |
+
# Keys may be int (msgpack) or str (JSON) β normalise to int.
|
| 3877 |
+
# Drop entries with delivery timestep <= current timestep (already processed).
|
| 3878 |
+
node_ids_set = set(self.nodes.keys())
|
| 3879 |
+
for ts_key, entries in data.get("delay_buffer", {}).items():
|
| 3880 |
+
ts = int(ts_key)
|
| 3881 |
+
if ts <= self.timestep:
|
| 3882 |
+
continue # stale β would have fired before save
|
| 3883 |
+
valid = [(str(nid), float(curr)) for nid, curr in entries
|
| 3884 |
+
if str(nid) in node_ids_set]
|
| 3885 |
+
if valid:
|
| 3886 |
+
self._delay_buffer[ts] = valid
|
| 3887 |
+
|
| 3888 |
# Restore synapses
|
| 3889 |
for sid, sd in data.get("synapses", {}).items():
|
| 3890 |
syn = Synapse(
|
|
|
|
| 4131 |
),
|
| 4132 |
]
|
| 4133 |
|
| 4134 |
+
# v0.4.2: Restore zero-firing circuit breaker streak count.
|
| 4135 |
+
# Without this a subprocess always starts at 0, losing continuity
|
| 4136 |
+
# and potentially masking persistent silence across calls.
|
| 4137 |
+
self._steps_since_last_fire = data.get("steps_since_last_fire", 0)
|
| 4138 |
+
|
| 4139 |
+
# v0.4.2: Restore homeostatic scaling counter.
|
| 4140 |
+
# HomeostaticRule.__init__ always sets _steps_since_scaling = 0 above.
|
| 4141 |
+
# Restore the saved value so homeostatic scaling fires at the correct
|
| 4142 |
+
# interval regardless of how many subprocess boundaries have been crossed.
|
| 4143 |
+
homeostatic_steps = data.get("homeostatic_steps_since_scaling", 0)
|
| 4144 |
+
for rule in self._plasticity_rules:
|
| 4145 |
+
if isinstance(rule, HomeostaticRule):
|
| 4146 |
+
rule._steps_since_scaling = homeostatic_steps
|
| 4147 |
+
break
|
| 4148 |
+
|
| 4149 |
self._dirty_nodes.clear()
|
| 4150 |
self._dirty_synapses.clear()
|
| 4151 |
self._dirty_hyperedges.clear()
|
neurograph_migrate.py
CHANGED
|
@@ -29,6 +29,9 @@ Schema Version History:
|
|
| 29 |
version bump for tracking)
|
| 30 |
0.4.1 β Phase 4.1: Consolidation Lifecycle (ConsolidationState enum,
|
| 31 |
synapse salience, hyperedge creation_time, consolidation config)
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
Grok Review Changelog (v0.7.1):
|
| 34 |
No code changes. Grok's suggestions for neurograph_migrate.py evaluated:
|
|
@@ -64,10 +67,10 @@ except ImportError:
|
|
| 64 |
|
| 65 |
|
| 66 |
# Ordered list of all schema versions
|
| 67 |
-
SCHEMA_VERSIONS = ["0.1.0", "0.2.0", "0.2.5", "0.3.0", "0.3.5", "0.4.0", "0.4.1"]
|
| 68 |
|
| 69 |
# Current target version
|
| 70 |
-
CURRENT_VERSION = "0.4.
|
| 71 |
|
| 72 |
|
| 73 |
def _version_tuple(v: str) -> Tuple[int, ...]:
|
|
@@ -242,6 +245,25 @@ def _migrate_0_4_0_to_0_4_1(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 242 |
return data
|
| 243 |
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
# Migration registry: (from_version, to_version) β migration function
|
| 246 |
MIGRATIONS: List[Tuple[str, str, Callable]] = [
|
| 247 |
("0.1.0", "0.2.0", _migrate_0_1_0_to_0_2_0),
|
|
@@ -250,6 +272,7 @@ MIGRATIONS: List[Tuple[str, str, Callable]] = [
|
|
| 250 |
("0.3.0", "0.3.5", _migrate_0_3_0_to_0_3_5),
|
| 251 |
("0.3.5", "0.4.0", _migrate_0_3_5_to_0_4_0),
|
| 252 |
("0.4.0", "0.4.1", _migrate_0_4_0_to_0_4_1),
|
|
|
|
| 253 |
]
|
| 254 |
|
| 255 |
|
|
|
|
| 29 |
version bump for tracking)
|
| 30 |
0.4.1 β Phase 4.1: Consolidation Lifecycle (ConsolidationState enum,
|
| 31 |
synapse salience, hyperedge creation_time, consolidation config)
|
| 32 |
+
0.4.2 β Hibernation fix: delay_buffer, recent_spikes, steps_since_last_fire,
|
| 33 |
+
homeostatic_steps_since_scaling serialized (no-op migration β new
|
| 34 |
+
fields are optional in _deserialize, defaults applied if absent)
|
| 35 |
|
| 36 |
Grok Review Changelog (v0.7.1):
|
| 37 |
No code changes. Grok's suggestions for neurograph_migrate.py evaluated:
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
# Ordered list of all schema versions
|
| 70 |
+
SCHEMA_VERSIONS = ["0.1.0", "0.2.0", "0.2.5", "0.3.0", "0.3.5", "0.4.0", "0.4.1", "0.4.2"]
|
| 71 |
|
| 72 |
# Current target version
|
| 73 |
+
CURRENT_VERSION = "0.4.2"
|
| 74 |
|
| 75 |
|
| 76 |
def _version_tuple(v: str) -> Tuple[int, ...]:
|
|
|
|
| 245 |
return data
|
| 246 |
|
| 247 |
|
| 248 |
+
def _migrate_0_4_1_to_0_4_2(data: Dict[str, Any]) -> Dict[str, Any]:
|
| 249 |
+
"""Phase 4.1 β 4.2: Hibernation fix β no-op schema migration.
|
| 250 |
+
|
| 251 |
+
The four new fields (delay_buffer, recent_spikes, steps_since_last_fire,
|
| 252 |
+
homeostatic_steps_since_scaling) are optional in _deserialize() and
|
| 253 |
+
default to zero/empty when absent. There is no data to backfill β old
|
| 254 |
+
checkpoints simply won't have in-flight state to restore, which is
|
| 255 |
+
correct: the buffer was empty at save time in the old code. The version
|
| 256 |
+
bump ensures that once a checkpoint is saved by v0.4.2 code it carries
|
| 257 |
+
the hibernation fields and won't be needlessly migrated again.
|
| 258 |
+
"""
|
| 259 |
+
data.setdefault("delay_buffer", {})
|
| 260 |
+
data.setdefault("recent_spikes", {})
|
| 261 |
+
data.setdefault("steps_since_last_fire", 0)
|
| 262 |
+
data.setdefault("homeostatic_steps_since_scaling", 0)
|
| 263 |
+
data["version"] = "0.4.2"
|
| 264 |
+
return data
|
| 265 |
+
|
| 266 |
+
|
| 267 |
# Migration registry: (from_version, to_version) β migration function
|
| 268 |
MIGRATIONS: List[Tuple[str, str, Callable]] = [
|
| 269 |
("0.1.0", "0.2.0", _migrate_0_1_0_to_0_2_0),
|
|
|
|
| 272 |
("0.3.0", "0.3.5", _migrate_0_3_0_to_0_3_5),
|
| 273 |
("0.3.5", "0.4.0", _migrate_0_3_5_to_0_4_0),
|
| 274 |
("0.4.0", "0.4.1", _migrate_0_4_0_to_0_4_1),
|
| 275 |
+
("0.4.1", "0.4.2", _migrate_0_4_1_to_0_4_2),
|
| 276 |
]
|
| 277 |
|
| 278 |
|