Executor-Tyrant-Framework Claude Opus 4.6 (1M context) commited on
Commit
bec55ca
Β·
1 Parent(s): fccad81

Wire ng_tract BTF into the organism: two tracts, no preclassification

Browse files

Replaces the torch-pickled state blob with the canonical ecosystem
pattern (Rust Substrate Layer #119). Embeddings are now numpy f32
end-to-end β€” no more Python-list inflation on every deposit.

Two tracts, each doing one biological job (Substrate As Cortex):
- experience.tract: raw text via ng_tract.deposit_experience.
Full bytes, source-annotated, append-forever. Law 7 sensory
stream. Source tag ('user' / 'nuwave_response') is origin
annotation, not classification.
- outcomes.tract: ng_tract.deposit_outcome per node, embedding
as numpy f32 (zero-copy), metadata via msgpack. This is the
neuromodulatory learning signal β€” success bool is dopamine
polarity, not a classification label.

Preclassification purged from deposit metadata:
- No more "type": "experience" / "response" on node metadata β€”
the substrate discovers that distinction via STDP co-firing
- No more text[:200] truncation at deposit time β€” full text
enters; Pith bucket decides what to truncate at extraction
- No more "query": user_message[:100] on responses β€” the causal
link is recoverable from timestamp adjacency in the tract

Embeddings moved off node.metadata into self._embeddings side-table
(numpy f32 dict). Graph checkpoints stay lean. Pith reads from the
side-table directly β€” no np.array() conversion on the hot path.

Sidecar (nuwave_sidecar.pt) carries only substrate-dynamics state
that BTF doesn't naturally persist: graph checkpoint, amplitudes,
phases, node_content cache, stats. Never embeddings.

Legacy state migration: _apply_state pops embedding lists out of
node.metadata on restore and lifts them into self._embeddings as
numpy. Existing organism_state.pt on the hub still loads cleanly.

Hub push now uploads sidecar + both tracts. Tract files survive
container rebuilds and are available for future analysis buckets
(e.g. interleaved-benchmark neighborhood-overlap extraction).

Verified end-to-end: deposit β†’ step β†’ pith β†’ record_outcome β†’
warm restart β†’ legacy migration. All tracts appendable, numpy
embeddings round-trip zero-copy via embedding_as_numpy().

Refs: ~/docs/concepts/Rust Substrate Layer.md
~/docs/concepts/Substrate As Cortex.md
~/docs/prd/Binary_Tract_Format_v0.1.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. nuwave/organism.py +275 -74
  2. requirements.txt +2 -0
nuwave/organism.py CHANGED
@@ -16,6 +16,25 @@ communication protocol (Law 1). Raw experience in, classification
16
  only at extraction (Law 7).
17
 
18
  # ---- Changelog ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  # [2026-04-06] Claude Code (Opus 4.6) β€” Initial organism wiring
20
  # What: Full substrate loop for HuggingFace NuWave demo
21
  # Why: Baseline proved 47.2% with crude string KISS. Real substrate
@@ -36,6 +55,13 @@ from typing import Any, Dict, List, Optional, Tuple
36
 
37
  import numpy as np
38
 
 
 
 
 
 
 
 
39
  logger = logging.getLogger("nuwave.organism")
40
 
41
  # Substrate path β€” added temporarily during import, not permanently
@@ -52,14 +78,25 @@ class NuWaveOrganism:
52
  organism.record_outcome(user_message, response, success=True)
53
  """
54
 
55
- # Hub persistence β€” survives container rebuilds, sleeps, everything
 
 
 
 
56
  HUB_REPO = "Executor-Tyrant-Framework/nuwave-substrate-state"
57
- HUB_FILENAME = "organism_state.pt"
 
 
 
 
58
 
59
  def __init__(self, state_path: str = "/tmp/nuwave_substrate"):
60
  self._state_path = state_path
61
  os.makedirs(state_path, exist_ok=True)
62
- self._local_state_path = os.path.join(state_path, "organism_state.pt")
 
 
 
63
  self._hf_token = os.environ.get("HF_TOKEN", None)
64
 
65
  # Initialize substrate
@@ -67,7 +104,14 @@ class NuWaveOrganism:
67
  self._embed_fn = None
68
  self._step_result = None
69
  self._step_count = 0
70
- self._node_content: Dict[str, str] = {} # node_id β†’ content for Pith
 
 
 
 
 
 
 
71
 
72
  # Amplitude field β€” per-node complex amplitude for interference dynamics
73
  # amplitude: how strongly this node contributes (grown by use, decayed by time)
@@ -118,37 +162,108 @@ class NuWaveOrganism:
118
  logger.info("No saved state found β€” starting fresh organism")
119
 
120
  def _try_restore_local(self) -> bool:
121
- """Restore from local state file."""
122
- if not os.path.exists(self._local_state_path):
123
- return False
124
- try:
125
- import torch
126
- state = torch.load(self._local_state_path, map_location="cpu", weights_only=False)
127
- return self._apply_state(state, source="local")
128
- except Exception as exc:
129
- logger.warning("Local restore failed: %s", exc)
130
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  def _try_restore_hub(self) -> bool:
133
- """Pull state from HuggingFace dataset repo."""
 
 
 
 
 
 
134
  if not self._hf_token:
135
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  try:
137
- from huggingface_hub import hf_hub_download
138
- local = hf_hub_download(
139
- self.HUB_REPO, self.HUB_FILENAME,
140
- repo_type="dataset", local_dir=self._state_path,
141
- token=self._hf_token,
142
- )
143
- import torch
144
- state = torch.load(local, map_location="cpu", weights_only=False)
145
- return self._apply_state(state, source="hub")
 
 
 
 
 
 
146
  except Exception as exc:
147
- logger.info("No hub state: %s", exc)
148
- return False
149
 
150
  def _apply_state(self, state: dict, source: str) -> bool:
151
- """Apply a loaded state dict to the organism."""
152
  try:
153
  # Graph checkpoint β€” stored as JSON string
154
  graph_json = state.get('graph_checkpoint_json')
@@ -159,6 +274,23 @@ class NuWaveOrganism:
159
  self._graph.restore(tmp)
160
  os.remove(tmp)
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  # Content map
163
  self._node_content = state.get('node_content', {})
164
 
@@ -172,13 +304,14 @@ class NuWaveOrganism:
172
  self._phases = state.get('phases', {})
173
 
174
  logger.info(
175
- "Organism restored from %s: %d nodes, %d syn, %d content, step %d, %d amplitudes",
176
  source,
177
  len(self._graph.nodes) if self._graph else 0,
178
  len(self._graph.synapses) if self._graph else 0,
179
  len(self._node_content),
180
  self._step_count,
181
  len(self._amplitudes),
 
182
  )
183
  return True
184
  except Exception as exc:
@@ -265,24 +398,46 @@ class NuWaveOrganism:
265
  return None
266
 
267
  try:
268
- embedding = self._embed_fn(text)
 
 
 
 
 
 
 
 
 
 
269
  node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
270
 
271
- # Create node in the SNN
 
 
272
  node = self._graph.create_node(
273
  node_id=node_id,
274
- metadata={
275
- "content": text[:200],
276
- "step": self._step_count,
277
- "type": "experience",
278
- },
279
  )
280
 
281
- # Store embedding in metadata for Pith similarity search
282
- node.metadata['embedding'] = embedding.tolist()
 
 
 
 
283
 
284
- # Track for Pith retrieval
285
- self._node_content[node_id] = text[:200]
 
 
 
 
 
 
 
 
 
 
286
 
287
  # Initialize amplitude for new node β€” starts at 1.0 (just deposited)
288
  self._amplitudes[node_id] = 1.0
@@ -300,11 +455,11 @@ class NuWaveOrganism:
300
  for existing_id, existing_node in self._graph.nodes.items():
301
  if existing_id == node_id:
302
  continue
303
- existing_emb = existing_node.metadata.get('embedding') if existing_node.metadata else None
 
304
  if existing_emb is None:
305
  continue
306
 
307
- existing_emb = np.array(existing_emb, dtype=np.float32)
308
  sim = float(np.dot(embedding, existing_emb) /
309
  (np.linalg.norm(embedding) * np.linalg.norm(existing_emb) + 1e-8))
310
 
@@ -482,7 +637,7 @@ class NuWaveOrganism:
482
 
483
  try:
484
  import math
485
- query_embedding = self._embed_fn(query)
486
 
487
  # Compute per-node Born rule score.
488
  # The query interferes with each node's amplitude:
@@ -490,12 +645,14 @@ class NuWaveOrganism:
490
  # existing amplitude is the base strength.
491
  scored = []
492
 
493
- for nid, node in self._graph.nodes.items():
494
- emb = node.metadata.get('embedding') if node.metadata else None
495
- if emb is None:
 
 
 
496
  continue
497
 
498
- node_emb = np.array(emb, dtype=np.float32)
499
  sim = float(np.dot(query_embedding, node_emb) /
500
  (np.linalg.norm(query_embedding) * np.linalg.norm(node_emb) + 1e-8))
501
 
@@ -568,23 +725,45 @@ class NuWaveOrganism:
568
  return
569
 
570
  try:
 
 
 
 
 
 
 
 
 
 
 
 
571
  # Response as raw experience β€” create node, stimulate, reward
572
- response_embedding = self._embed_fn(response)
573
  node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
574
 
 
 
 
575
  node = self._graph.create_node(
576
  node_id=node_id,
577
- metadata={
578
- "content": response[:200],
579
- "type": "response",
580
- "step": self._step_count,
581
- "query": user_message[:100],
582
- },
583
  )
584
- node.metadata['embedding'] = response_embedding.tolist()
585
 
586
  self._graph.stimulate(node_id, current=1.0)
587
- self._node_content[node_id] = response[:200]
 
 
 
 
 
 
 
 
 
 
 
 
588
 
589
  # Reward signal β€” the outcome closes the loop
590
  # Strong reward strengthens synapses between co-firing nodes via STDP.
@@ -605,14 +784,20 @@ class NuWaveOrganism:
605
  return dict(self.stats)
606
 
607
  def save(self):
608
- """Persist all state β€” single file, local + hub.
 
 
 
 
 
609
 
610
- One consolidated state dict. Saved locally for warm restart.
611
- Pushed to HF hub for survival across rebuilds.
 
612
  """
613
  import torch
614
 
615
- # Build consolidated state
616
  state = {
617
  'node_content': self._node_content,
618
  'stats': dict(self.stats),
@@ -620,9 +805,14 @@ class NuWaveOrganism:
620
  'phases': self._phases,
621
  }
622
 
623
- # Graph checkpoint β€” read as raw JSON string
 
 
624
  if self._graph:
625
  try:
 
 
 
626
  tmp = os.path.join(self._state_path, "_tmp_save.ckpt")
627
  self._graph.checkpoint(tmp)
628
  with open(tmp, 'r') as f:
@@ -631,32 +821,43 @@ class NuWaveOrganism:
631
  except Exception as exc:
632
  logger.debug("Graph checkpoint failed: %s", exc)
633
 
634
- # Save locally
635
  try:
636
- torch.save(state, self._local_state_path)
637
  except Exception as exc:
638
- logger.debug("Local save failed: %s", exc)
639
 
640
- # Push to hub β€” survives everything
641
  self._push_to_hub()
642
 
643
  def _push_to_hub(self):
644
- """Push local state file to HF dataset repo."""
645
- if not self._hf_token or not os.path.exists(self._local_state_path):
646
  return
647
  try:
648
  from huggingface_hub import HfApi, create_repo
649
- # Ensure repo exists
650
  create_repo(self.HUB_REPO, repo_type="dataset", exist_ok=True,
651
  private=True, token=self._hf_token)
652
  api = HfApi()
653
- api.upload_file(
654
- path_or_fileobj=self._local_state_path,
655
- path_in_repo=self.HUB_FILENAME,
656
- repo_id=self.HUB_REPO,
657
- repo_type="dataset",
658
- token=self._hf_token,
659
- )
 
 
 
 
 
 
 
 
 
 
 
 
660
  logger.info("State pushed to hub (%s)", self.HUB_REPO)
661
  except Exception as exc:
662
  logger.debug("Hub push failed (non-fatal): %s", exc)
 
16
  only at extraction (Law 7).
17
 
18
  # ---- Changelog ----
19
+ # [2026-04-15] Claude Code (Opus 4.6) β€” BTF wire-in (Rust Substrate Layer)
20
+ # What: Wire ng_tract BTF into the deposit/outcome path. Two tracts:
21
+ # experience.tract (raw text, Law 7 sensory stream, append forever)
22
+ # outcomes.tract (embedding + reward, neuromodulatory log).
23
+ # Embeddings moved off node.metadata into self._embeddings
24
+ # side-table β€” kept as numpy f32 the whole way, no .tolist().
25
+ # Preclassification purged from deposit metadata: no more
26
+ # "type": "experience"/"response" labels, no text[:200] truncation
27
+ # at deposit time. Full bytes into the experience tract.
28
+ # Why: See ~/docs/concepts/Substrate As Cortex.md β€” the substrate is
29
+ # cortex, not a database. Raw experience in, classification at
30
+ # extraction. ~/docs/concepts/Rust Substrate Layer.md β€” BTF is
31
+ # the canonical ecosystem pattern; embedding.tolist() is the
32
+ # exact inflation point BTF was built to eliminate (PRD Β§2).
33
+ # How: deposit_experience(str, source, path, content_type) for raw
34
+ # sensory input. deposit_outcome(ts, module, target, success,
35
+ # numpy_emb, [path], msgpack_meta) for the learning signal.
36
+ # Sidecar holds only substrate-derived state (amplitudes, phases,
37
+ # graph checkpoint, node_content cache, stats) β€” never embeddings.
38
  # [2026-04-06] Claude Code (Opus 4.6) β€” Initial organism wiring
39
  # What: Full substrate loop for HuggingFace NuWave demo
40
  # Why: Baseline proved 47.2% with crude string KISS. Real substrate
 
55
 
56
  import numpy as np
57
 
58
+ # BTF path β€” msgpack for metadata bytes, ng_tract for tract I/O.
59
+ # Hard requirement: wheel is vendored in the repo root, declared in
60
+ # requirements.txt. If import fails the organism should fail loudly,
61
+ # not silently fall back to inflated JSON.
62
+ import msgpack
63
+ import ng_tract
64
+
65
  logger = logging.getLogger("nuwave.organism")
66
 
67
  # Substrate path β€” added temporarily during import, not permanently
 
78
  organism.record_outcome(user_message, response, success=True)
79
  """
80
 
81
+ # Hub persistence β€” survives container rebuilds, sleeps, everything.
82
+ # Three artifacts now: sidecar (amplitudes, phases, graph, stats, content
83
+ # cache) + two tracts (experience.tract, outcomes.tract). Tracts are the
84
+ # canonical form for embeddings and Law 7 sensory log; sidecar is the
85
+ # substrate-dynamics state that BTF doesn't carry.
86
  HUB_REPO = "Executor-Tyrant-Framework/nuwave-substrate-state"
87
+ HUB_SIDECAR_FILENAME = "nuwave_sidecar.pt"
88
+ HUB_EXP_TRACT_FILENAME = "experience.tract"
89
+ HUB_OUTCOMES_TRACT_FILENAME = "outcomes.tract"
90
+ # Legacy β€” read-only, for migration from the pre-BTF state format.
91
+ HUB_LEGACY_FILENAME = "organism_state.pt"
92
 
93
  def __init__(self, state_path: str = "/tmp/nuwave_substrate"):
94
  self._state_path = state_path
95
  os.makedirs(state_path, exist_ok=True)
96
+ self._local_sidecar_path = os.path.join(state_path, self.HUB_SIDECAR_FILENAME)
97
+ self._local_legacy_path = os.path.join(state_path, self.HUB_LEGACY_FILENAME)
98
+ self._exp_tract_path = os.path.join(state_path, self.HUB_EXP_TRACT_FILENAME)
99
+ self._outcomes_tract_path = os.path.join(state_path, self.HUB_OUTCOMES_TRACT_FILENAME)
100
  self._hf_token = os.environ.get("HF_TOKEN", None)
101
 
102
  # Initialize substrate
 
104
  self._embed_fn = None
105
  self._step_result = None
106
  self._step_count = 0
107
+ self._node_content: Dict[str, str] = {} # node_id β†’ FULL text (no truncation)
108
+
109
+ # Embedding side-table β€” numpy f32, never a Python list. BTF outcomes
110
+ # tract is the persisted form; this dict is the hot-path runtime
111
+ # cache. Nodes in the Graph carry NO embeddings β€” keeps graph
112
+ # checkpoints small and eliminates the list↔numpy round-trip that
113
+ # BTF was built to kill (see Rust Substrate Layer doc Β§2).
114
+ self._embeddings: Dict[str, np.ndarray] = {}
115
 
116
  # Amplitude field β€” per-node complex amplitude for interference dynamics
117
  # amplitude: how strongly this node contributes (grown by use, decayed by time)
 
162
  logger.info("No saved state found β€” starting fresh organism")
163
 
164
  def _try_restore_local(self) -> bool:
165
+ """Restore from local sidecar; fall back to legacy format for migration.
166
+
167
+ New format (post-BTF): nuwave_sidecar.pt holds substrate-derived
168
+ state, no embeddings. Embeddings are restored from outcomes.tract.
169
+ Legacy format (pre-BTF): organism_state.pt β€” embeddings as Python
170
+ lists inside graph checkpoint metadata. One-shot migration at
171
+ load time: lists are lifted into self._embeddings as numpy.
172
+ """
173
+ for path, source in [
174
+ (self._local_sidecar_path, "local-sidecar"),
175
+ (self._local_legacy_path, "local-legacy"),
176
+ ]:
177
+ if not os.path.exists(path):
178
+ continue
179
+ try:
180
+ import torch
181
+ state = torch.load(path, map_location="cpu", weights_only=False)
182
+ if self._apply_state(state, source=source):
183
+ # After sidecar/legacy applied, pull embeddings from
184
+ # outcomes tract (canonical form). Tract may or may
185
+ # not exist yet β€” this is best-effort.
186
+ self._restore_embeddings_from_tract()
187
+ return True
188
+ except Exception as exc:
189
+ logger.warning("Local restore (%s) failed: %s", source, exc)
190
+ return False
191
 
192
  def _try_restore_hub(self) -> bool:
193
+ """Pull sidecar + tracts from HuggingFace dataset repo.
194
+
195
+ Tries the new sidecar filename first. Falls back to legacy
196
+ organism_state.pt if present. Tracts are pulled alongside so
197
+ the local cache has them for analysis buckets and for embedding
198
+ restoration.
199
+ """
200
  if not self._hf_token:
201
  return False
202
+ from huggingface_hub import hf_hub_download
203
+
204
+ def _pull(filename: str) -> Optional[str]:
205
+ try:
206
+ return hf_hub_download(
207
+ self.HUB_REPO, filename,
208
+ repo_type="dataset", local_dir=self._state_path,
209
+ token=self._hf_token,
210
+ )
211
+ except Exception as exc:
212
+ logger.info("Hub %s absent: %s", filename, exc)
213
+ return None
214
+
215
+ # Pull tracts first β€” they're the canonical embedding source.
216
+ _pull(self.HUB_EXP_TRACT_FILENAME)
217
+ _pull(self.HUB_OUTCOMES_TRACT_FILENAME)
218
+
219
+ # Try new-format sidecar, then legacy.
220
+ for filename, source in [
221
+ (self.HUB_SIDECAR_FILENAME, "hub-sidecar"),
222
+ (self.HUB_LEGACY_FILENAME, "hub-legacy"),
223
+ ]:
224
+ local = _pull(filename)
225
+ if not local:
226
+ continue
227
+ try:
228
+ import torch
229
+ state = torch.load(local, map_location="cpu", weights_only=False)
230
+ if self._apply_state(state, source=source):
231
+ self._restore_embeddings_from_tract()
232
+ return True
233
+ except Exception as exc:
234
+ logger.warning("Hub apply (%s) failed: %s", source, exc)
235
+ return False
236
+
237
+ def _restore_embeddings_from_tract(self):
238
+ """Populate self._embeddings from outcomes.tract (canonical form).
239
+
240
+ Walks the tract in order; later entries win for repeated node_ids
241
+ (outcome tract is append-only β€” a node's latest reward-modulated
242
+ embedding is the current one). Zero-copy via embedding_as_numpy.
243
+ """
244
+ if not os.path.exists(self._outcomes_tract_path):
245
+ return
246
  try:
247
+ with open(self._outcomes_tract_path, 'rb') as f:
248
+ raw = f.read()
249
+ count = 0
250
+ for entry in ng_tract.TractReader(raw):
251
+ # PyOutcomeEntry has .target_id, .embedding_as_numpy()
252
+ tid = getattr(entry, 'target_id', None)
253
+ if not tid:
254
+ continue
255
+ try:
256
+ self._embeddings[tid] = entry.embedding_as_numpy()
257
+ count += 1
258
+ except Exception:
259
+ continue
260
+ logger.info("Restored %d embeddings from outcomes.tract (unique=%d)",
261
+ count, len(self._embeddings))
262
  except Exception as exc:
263
+ logger.warning("Tract embedding restore failed: %s", exc)
 
264
 
265
  def _apply_state(self, state: dict, source: str) -> bool:
266
+ """Apply a loaded state dict. Handles new + legacy formats."""
267
  try:
268
  # Graph checkpoint β€” stored as JSON string
269
  graph_json = state.get('graph_checkpoint_json')
 
274
  self._graph.restore(tmp)
275
  os.remove(tmp)
276
 
277
+ # LEGACY MIGRATION: pre-BTF checkpoints stored embeddings
278
+ # as Python lists inside node.metadata['embedding']. Lift
279
+ # them into self._embeddings as numpy f32, strip from
280
+ # metadata so future checkpoints stay lean.
281
+ migrated = 0
282
+ for nid, node in self._graph.nodes.items():
283
+ if node.metadata and 'embedding' in node.metadata:
284
+ emb = node.metadata.pop('embedding')
285
+ if isinstance(emb, list):
286
+ self._embeddings[nid] = np.asarray(emb, dtype=np.float32)
287
+ migrated += 1
288
+ elif isinstance(emb, np.ndarray):
289
+ self._embeddings[nid] = emb.astype(np.float32, copy=False)
290
+ migrated += 1
291
+ if migrated:
292
+ logger.info("Migrated %d legacy embeddings from graph metadata", migrated)
293
+
294
  # Content map
295
  self._node_content = state.get('node_content', {})
296
 
 
304
  self._phases = state.get('phases', {})
305
 
306
  logger.info(
307
+ "Organism restored from %s: %d nodes, %d syn, %d content, step %d, %d amps, %d embs",
308
  source,
309
  len(self._graph.nodes) if self._graph else 0,
310
  len(self._graph.synapses) if self._graph else 0,
311
  len(self._node_content),
312
  self._step_count,
313
  len(self._amplitudes),
314
+ len(self._embeddings),
315
  )
316
  return True
317
  except Exception as exc:
 
398
  return None
399
 
400
  try:
401
+ # Law 7: raw bytes go to the experience tract BEFORE any
402
+ # processing. Full text, no truncation, source-annotated.
403
+ # The substrate learns via STDP co-firing, not via curated fields.
404
+ try:
405
+ ng_tract.deposit_experience(
406
+ text, "user", self._exp_tract_path, "text"
407
+ )
408
+ except Exception as exc:
409
+ logger.debug("Experience tract write failed: %s", exc)
410
+
411
+ embedding = np.asarray(self._embed_fn(text), dtype=np.float32)
412
  node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
413
 
414
+ # Create node in the SNN. Metadata carries biological timestamp
415
+ # only β€” no "type" label, no "content" curation. The retina
416
+ # does not classify photons; V1 discovers features via dynamics.
417
  node = self._graph.create_node(
418
  node_id=node_id,
419
+ metadata={"step": self._step_count},
 
 
 
 
420
  )
421
 
422
+ # Embedding lives in the side-table as numpy f32 (zero inflation).
423
+ # Outcomes tract is the persisted form; this dict is runtime cache.
424
+ self._embeddings[node_id] = embedding
425
+
426
+ # Full-text content cache β€” Pith trims at extraction, not here.
427
+ self._node_content[node_id] = text
428
 
429
+ # Neuromodulatory signal to outcomes tract. success=True is
430
+ # the dopamine polarity for sensory deposits (reward the act
431
+ # of observing); the actual learning signal from a full
432
+ # interaction flows through record_outcome() after response.
433
+ try:
434
+ ng_tract.deposit_outcome(
435
+ time.time(), "nuwave", node_id, True, embedding,
436
+ [self._outcomes_tract_path],
437
+ msgpack.packb({"step": self._step_count, "source": "user"}),
438
+ )
439
+ except Exception as exc:
440
+ logger.debug("Outcomes tract write failed: %s", exc)
441
 
442
  # Initialize amplitude for new node β€” starts at 1.0 (just deposited)
443
  self._amplitudes[node_id] = 1.0
 
455
  for existing_id, existing_node in self._graph.nodes.items():
456
  if existing_id == node_id:
457
  continue
458
+ # Embeddings live in the side-table now, not node.metadata.
459
+ existing_emb = self._embeddings.get(existing_id)
460
  if existing_emb is None:
461
  continue
462
 
 
463
  sim = float(np.dot(embedding, existing_emb) /
464
  (np.linalg.norm(embedding) * np.linalg.norm(existing_emb) + 1e-8))
465
 
 
637
 
638
  try:
639
  import math
640
+ query_embedding = np.asarray(self._embed_fn(query), dtype=np.float32)
641
 
642
  # Compute per-node Born rule score.
643
  # The query interferes with each node's amplitude:
 
645
  # existing amplitude is the base strength.
646
  scored = []
647
 
648
+ # Iterate the side-table, not graph.nodes β€” embeddings no
649
+ # longer live in node.metadata. Nodes without embeddings
650
+ # (e.g. legacy graph entries) are skipped naturally.
651
+ for nid in self._graph.nodes.keys():
652
+ node_emb = self._embeddings.get(nid)
653
+ if node_emb is None:
654
  continue
655
 
 
656
  sim = float(np.dot(query_embedding, node_emb) /
657
  (np.linalg.norm(query_embedding) * np.linalg.norm(node_emb) + 1e-8))
658
 
 
725
  return
726
 
727
  try:
728
+ # Raw response bytes β†’ experience tract (Law 7). Source tag
729
+ # distinguishes origin ("nuwave_response" vs "user") β€” that's
730
+ # origin annotation, not classification. The causal link to
731
+ # the query is derivable from timestamp adjacency in the
732
+ # tract; no need to curate a "query" field at deposit time.
733
+ try:
734
+ ng_tract.deposit_experience(
735
+ response, "nuwave_response", self._exp_tract_path, "text"
736
+ )
737
+ except Exception as exc:
738
+ logger.debug("Experience tract write failed: %s", exc)
739
+
740
  # Response as raw experience β€” create node, stimulate, reward
741
+ response_embedding = np.asarray(self._embed_fn(response), dtype=np.float32)
742
  node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
743
 
744
+ # Metadata carries biological timestamp only β€” no type label,
745
+ # no query field, no truncated content. The substrate discovers
746
+ # response-vs-experience distinction via STDP co-firing.
747
  node = self._graph.create_node(
748
  node_id=node_id,
749
+ metadata={"step": self._step_count},
 
 
 
 
 
750
  )
751
+ self._embeddings[node_id] = response_embedding
752
 
753
  self._graph.stimulate(node_id, current=1.0)
754
+ self._node_content[node_id] = response
755
+
756
+ # Neuromodulatory deposit β€” the reward polarity is THE
757
+ # learning signal. This is biology (dopamine modulates STDP),
758
+ # not preclassification. Real outcome bool reaches the tract.
759
+ try:
760
+ ng_tract.deposit_outcome(
761
+ time.time(), "nuwave", node_id, success, response_embedding,
762
+ [self._outcomes_tract_path],
763
+ msgpack.packb({"step": self._step_count, "source": "nuwave_response"}),
764
+ )
765
+ except Exception as exc:
766
+ logger.debug("Outcomes tract write failed: %s", exc)
767
 
768
  # Reward signal β€” the outcome closes the loop
769
  # Strong reward strengthens synapses between co-firing nodes via STDP.
 
784
  return dict(self.stats)
785
 
786
  def save(self):
787
+ """Persist all state β€” sidecar + tracts, local + hub.
788
+
789
+ Sidecar holds substrate-dynamics state that BTF doesn't carry:
790
+ graph checkpoint (synapses/hyperedges/predictions), amplitudes,
791
+ phases, node_content cache, stats. NO embeddings β€” those live
792
+ in outcomes.tract as numpy f32.
793
 
794
+ Tracts are append-only β€” written as deposits happen, not here.
795
+ This method only needs to save the sidecar and push everything
796
+ to the hub.
797
  """
798
  import torch
799
 
800
+ # Build sidecar state β€” no embeddings.
801
  state = {
802
  'node_content': self._node_content,
803
  'stats': dict(self.stats),
 
805
  'phases': self._phases,
806
  }
807
 
808
+ # Graph checkpoint β€” read as raw JSON string. If any legacy
809
+ # embeddings slipped into node.metadata, strip them here so
810
+ # future checkpoints stay lean.
811
  if self._graph:
812
  try:
813
+ for node in self._graph.nodes.values():
814
+ if node.metadata and 'embedding' in node.metadata:
815
+ del node.metadata['embedding']
816
  tmp = os.path.join(self._state_path, "_tmp_save.ckpt")
817
  self._graph.checkpoint(tmp)
818
  with open(tmp, 'r') as f:
 
821
  except Exception as exc:
822
  logger.debug("Graph checkpoint failed: %s", exc)
823
 
824
+ # Save sidecar locally
825
  try:
826
+ torch.save(state, self._local_sidecar_path)
827
  except Exception as exc:
828
+ logger.debug("Local sidecar save failed: %s", exc)
829
 
830
+ # Push sidecar + tracts to hub
831
  self._push_to_hub()
832
 
833
  def _push_to_hub(self):
834
+ """Push sidecar + both tracts to HF dataset repo."""
835
+ if not self._hf_token:
836
  return
837
  try:
838
  from huggingface_hub import HfApi, create_repo
 
839
  create_repo(self.HUB_REPO, repo_type="dataset", exist_ok=True,
840
  private=True, token=self._hf_token)
841
  api = HfApi()
842
+
843
+ for local_path, hub_name in [
844
+ (self._local_sidecar_path, self.HUB_SIDECAR_FILENAME),
845
+ (self._exp_tract_path, self.HUB_EXP_TRACT_FILENAME),
846
+ (self._outcomes_tract_path, self.HUB_OUTCOMES_TRACT_FILENAME),
847
+ ]:
848
+ if not os.path.exists(local_path):
849
+ continue
850
+ try:
851
+ api.upload_file(
852
+ path_or_fileobj=local_path,
853
+ path_in_repo=hub_name,
854
+ repo_id=self.HUB_REPO,
855
+ repo_type="dataset",
856
+ token=self._hf_token,
857
+ )
858
+ except Exception as exc:
859
+ logger.debug("Hub push of %s failed (non-fatal): %s", hub_name, exc)
860
+
861
  logger.info("State pushed to hub (%s)", self.HUB_REPO)
862
  except Exception as exc:
863
  logger.debug("Hub push failed (non-fatal): %s", exc)
requirements.txt CHANGED
@@ -3,3 +3,5 @@ torch>=2.0.0
3
  accelerate>=0.27.0
4
  onnxruntime>=1.16.0
5
  tokenizers>=0.15.0
 
 
 
3
  accelerate>=0.27.0
4
  onnxruntime>=1.16.0
5
  tokenizers>=0.15.0
6
+ msgpack>=1.0.0
7
+ ./ng_tract-0.1.0-cp312-abi3-manylinux_2_34_x86_64.whl