Executor-Tyrant-Framework Claude Sonnet 4.6 commited on
Commit
212a158
Β·
1 Parent(s): cf762ba

sync(neuro_foundation): v0.4.2 hibernation fix from canonical NeuroGraph

Browse files

Synced 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>

Files changed (2) hide show
  1. neuro_foundation.py +72 -1
  2. 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.1",
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.1"
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