🔬 Labs — Experimental Features¶
Status: Research / Future Work
These features are under active research and planned for implementation in the
labsbranch. They are not yet available in the main release.
Neuromodulatory Gain Control¶
Concept¶
Dynamic retrieval tuning via simulated neurotransmitter modulation. Rather than using static cognitive profiles, the system would maintain a runtime neuromodulatory state that continuously adjusts retrieval parameters based on the agent's recent activity, outcomes, and context.
Biological Basis¶
The brain's retrieval characteristics aren't fixed — they shift moment-to-moment based on neuromodulatory tone. A developer who just encountered a production outage has elevated norepinephrine, which sharpens recency bias and narrows attention. A developer brainstorming during a design review has elevated serotonin, which broadens associative scope.
Currently, Spector models this via discrete Cognitive Profiles (DEBUGGING, EXPLORING, etc.). Neuromodulatory Gain Control would replace discrete switching with continuous, gradient modulation.
Proposed Architecture¶
flowchart TD
subgraph "Neuromodulatory State"
ACh["Acetylcholine<br/>attention sharpness"]
5HT["Serotonin<br/>retrieval breadth"]
DA["Dopamine<br/>novelty seeking"]
NE["Norepinephrine<br/>urgency bias"]
end
subgraph "Retrieval Modulation"
TM["Tag Match Strictness"]
LS["Lateral Scope"]
NW["Novelty Weight"]
RB["Recency Bias"]
end
ACh --> TM
5HT --> LS
DA --> NW
NE --> RB
subgraph "Inputs"
O["Outcome Feedback<br/>(reinforce calls)"]
C["Context Signals<br/>(tags, valence)"]
T["Temporal Patterns<br/>(query rate, errors)"]
end
O --> DA
O --> NE
C --> ACh
C --> 5HT
T --> NE
T --> DA
Modulation Parameters¶
| Neurotransmitter | Parameter Affected | Low Level Effect | High Level Effect |
|---|---|---|---|
| Acetylcholine (ACh) | tagMatchStrictness |
Loose tag gating (any overlap passes) | Strict tag gating (all bits must match) |
| Serotonin (5-HT) | lateralDistanceThreshold |
Narrow scope (close matches only) | Wide scope (cross-domain retrieval active) |
| Dopamine (DA) | noveltyWeight in ICNU |
Familiar memories preferred | Novel/surprising memories preferred |
| Norepinephrine (NE) | recencyBias |
All ages equal | Strong recency bias (last hour dominates) |
State Update Model¶
Each neurotransmitter level \(n_i(t)\) follows an exponential decay toward a baseline, with spikes driven by events:
Where:
- \(n_i^{\text{base}}\) — resting level for neurotransmitter \(i\) (profile-dependent)
- \(\tau_i\) — decay constant (how quickly it returns to baseline after a spike)
- \(\Delta n_i\) — event-driven spike (e.g., negative reinforcement → +NE, +ACh)
Example decay constants:
| Neurotransmitter | \(\tau\) | Rationale |
|---|---|---|
| ACh | 5 minutes | Attention shifts are fast |
| 5-HT | 30 minutes | Mood/scope changes are slow |
| DA | 10 minutes | Novelty-seeking is moderate |
| NE | 2 minutes | Urgency is very transient |
Event-to-Spike Mapping¶
| Event | ACh | 5-HT | DA | NE |
|---|---|---|---|---|
| Negative reinforcement (bug found) | +0.3 | -0.1 | — | +0.5 |
| Positive reinforcement (solution worked) | — | +0.2 | +0.3 | -0.2 |
| High recall latency (slow query) | +0.1 | — | — | +0.2 |
| Lateral result selected by agent | — | +0.3 | +0.2 | — |
| Repeated query (same topic, 3rd time) | +0.4 | -0.2 | -0.1 | — |
| No results found | — | +0.2 | +0.4 | +0.1 |
Implementation Sketch¶
public final class NeuromodulatoryState {
private volatile float acetylcholine = 0.5f; // baseline
private volatile float serotonin = 0.5f;
private volatile float dopamine = 0.5f;
private volatile float norepinephrine = 0.3f;
private volatile long lastUpdateMs = System.currentTimeMillis();
/**
* Applies exponential decay toward baseline, then adds event spikes.
*/
public synchronized void update(NeuroEvent... events) {
long now = System.currentTimeMillis();
float dtSeconds = (now - lastUpdateMs) / 1000f;
// Exponential decay toward baseline
acetylcholine = decayToward(acetylcholine, 0.5f, dtSeconds, TAU_ACH);
serotonin = decayToward(serotonin, 0.5f, dtSeconds, TAU_5HT);
dopamine = decayToward(dopamine, 0.5f, dtSeconds, TAU_DA);
norepinephrine = decayToward(norepinephrine, 0.3f, dtSeconds, TAU_NE);
// Apply event spikes
for (var event : events) {
acetylcholine = clamp(acetylcholine + event.deltaACh());
serotonin = clamp(serotonin + event.delta5HT());
dopamine = clamp(dopamine + event.deltaDA());
norepinephrine = clamp(norepinephrine + event.deltaNE());
}
lastUpdateMs = now;
}
/**
* Modulates RecallOptions based on current neuromodulatory state.
*/
public RecallOptions modulate(RecallOptions base) {
return base.toBuilder()
.lateralDistanceThreshold(base.lateralDistanceThreshold() * (2.0f * serotonin))
.hyperfocusBoost(base.hyperfocusBoost() * (1.0f + acetylcholine))
// ... other modulations
.build();
}
}
Dependencies & Complexity¶
- Dependencies: CognitiveProfile extensions, configurable ICNU weights
- Complexity: High — requires runtime state management, thread-safe neuromodulatory state, and careful calibration of decay constants and spike magnitudes
- Risk: Over-tuning can create oscillatory behavior (agent flip-flops between modes)
Executive Dysfunction Profile¶
Concept¶
A Hebbian-first recall path that bypasses vector similarity entirely. When the agent can't formulate a clear query (analogous to executive dysfunction), it falls back to associative recall: "what have I been thinking about recently?"
Biological Basis¶
In executive dysfunction, the prefrontal cortex struggles with top-down, goal-directed retrieval — the ability to say "I need to find X" and systematically search for it. However, bottom-up, associative recall remains intact — memories surface via association chains rather than directed search.
This is common in ADHD: you can't remember the specific thing you were looking for, but a tangential mention triggers a cascade of related memories. The STDP infrastructure now makes this possible — directed causal edges encode "thinking about A leads to thinking about B."
Proposed Architecture¶
flowchart TD
Q["Query: 'I was working on something...'"] --> D{"Executive<br/>Dysfunction<br/>Profile?"}
D -->|No| VS["Standard: Vector Search"]
D -->|Yes| STDP["STDP Edge Lookup"]
STDP --> CT["Get context tags from<br/>recent recall history"]
CT --> CE["Follow causal edges<br/>(predictive strength > 0.3)"]
CE --> R1["Memory: database config"]
CE --> R2["Memory: connection pool tuning"]
CE --> R3["Memory: timeout settings"]
VS --> S["6-Phase Scoring Pipeline"]
R1 --> M["Merge & Rank"]
R2 --> M
R3 --> M
S --> M
M --> F["Final Results"]
style STDP fill:#e74c3c,color:white
style CE fill:#e74c3c,color:white
Recall Algorithm¶
- Collect context tags from the last N recall results (default N=10)
- Query STDP edges for all causal predictions from those context tags
- Filter edges by predictive strength threshold (default > 0.3)
- Retrieve memories whose synaptic tags match the predicted tags
- Rank by STDP weight instead of vector similarity
- Optionally blend with a low-weight vector search for hybrid results
Key Differences from Standard Recall¶
| Aspect | Standard Recall | Executive Dysfunction |
|---|---|---|
| Primary signal | Vector similarity | STDP causal edges |
| Query requirement | Clear, specific query | Vague or absent query |
| Scoring formula | \(\alpha \cdot sim + \beta \cdot imp \cdot decay\) | \(stdp\_weight \cdot recency\) |
| Tag usage | Bloom filter pre-screen | Primary retrieval key |
| Lateral mode | Optional (DIVERGENT) | Always enabled |
Implementation Sketch¶
public List<CognitiveResult> recallAssociative(RecallOptions options) {
// Step 1: Collect recent context tags
Set<String> contextTags = recallHistory.recentTags(10);
// Step 2: Query STDP for causal predictions
Map<String, Float> predictions = new LinkedHashMap<>();
for (String tag : contextTags) {
tracker.getStdpEdgesFrom(tag).forEach((targetTag, weight) -> {
if (weight.weight() > 0.3f) {
predictions.merge(targetTag, weight.weight(), Math::max);
}
});
}
// Step 3: Encode predicted tags as a synaptic mask
long predictedMask = SynapticTagEncoder.encode(
predictions.keySet().toArray(String[]::new));
// Step 4: Scan with STDP-weighted scoring
var modifiedOptions = options.toBuilder()
.synapticTagMask(predictedMask)
.alpha(0.1f) // minimal vector similarity
.beta(0.9f) // importance-dominated
.build();
return recallPipeline.execute(queryVector, modifiedOptions);
}
Dependencies & Complexity¶
- Dependencies: Full STDP (Stage 3) ✅ Complete — directed, timestamped edges are live in
CoActivationTracker - Complexity: Medium — the STDP infrastructure is the hard part (done). Remaining work is the bypass routing logic and recall history tracking.
- Risk: Cold-start problem — STDP edges are empty until the agent has sufficient recall history
Two-Factor Memory Strength (Bjork & Bjork, 1992)¶
Concept¶
Separate retrieval strength R(t) from storage strength S(t). Currently, Spector uses a single decay curve based on age. The Two-Factor model captures a deeper truth: a memory's accessibility (can I recall it now?) and its durability (will it survive long-term?) are independent dimensions.
Biological Basis¶
The New Theory of Disuse (Bjork & Bjork, 1992) explains several well-known memory phenomena:
| Phenomenon | Explanation via R(t) and S(t) |
|---|---|
| Spacing effect | Spaced retrieval at low R(t) produces higher ΔS than massed retrieval at high R(t) |
| Testing effect | Active retrieval (low R(t)) boosts S(t) more than passive re-study |
| Savings in relearning | High S(t) memory with low R(t) relearns faster than a genuinely new memory |
| Tip-of-the-tongue | High S(t), very low R(t) — the memory is stored but temporarily inaccessible |
Mathematical Model¶
Retrieval strength decays with time since last access:
Where \(\lambda\) is the base decay rate (currently modeled by DecayStrategy.ageToBucket()).
Storage strength increases at each retrieval, with the boost inversely proportional to R(t):
This creates the spacing effect: when R(t) is near 0 (memory is hard to retrieve), the storage boost is maximal. When R(t) is near 1 (memory is easily retrieved), the storage boost is minimal.
Visual Model¶
graph LR
subgraph "Easy Retrieval (High R)"
E1["R(t) = 0.9"] --> E2["ΔS = 0.1 × S_gain"]
E2 --> E3["Low storage boost"]
end
subgraph "Hard Retrieval (Low R)"
H1["R(t) = 0.1"] --> H2["ΔS = 0.9 × S_gain"]
H2 --> H3["High storage boost"]
end
style E3 fill:#f39c12,color:white
style H3 fill:#27ae60,color:white
Integration with Existing Header Layout¶
The storage_strength field is already present in the V2 (48B) and V3 (64B) header layouts:
V2 Header Layout (48 bytes):
[32B core] — shared with V1
[1B arousal] Offset 32 — emotional intensity
[3B padding] Offset 33 — alignment
[4B storage_str] Offset 36 — S(t) ← THIS FIELD
[8B reserved] Offset 40 — future use
Current default: storage_strength = 1.0f for all new memories. The field is written and read but not yet used in scoring.
Proposed Scoring Integration¶
The current scoring formula:
Would become:
Where \(S(t)^{0.3}\) provides a gentle boost for well-stored memories without dominating the score.
Wiring into reinforce()¶
The reinforce() path in DefaultSpectorMemory already updates valence and recall count. The Two-Factor update would add:
public void reinforce(String memoryId, byte valence) {
MemoryLocation loc = index.lookup(memoryId);
MemorySegment segment = tierRouter.segmentFor(loc.type());
long offset = loc.offset();
// Existing: update valence
segment.set(LAYOUT_VALENCE, offset + OFFSET_VALENCE, valence);
// Existing: increment recall count (atomic CAS)
int recallCount = incrementRecallCount(segment, offset);
// NEW: Two-Factor update
if (layout.headerLayout().headerBytes() >= 48) { // V2+
long timestamp = segment.get(LAYOUT_TIMESTAMP, offset + OFFSET_TIMESTAMP);
float currentS = segment.get(LAYOUT_STORAGE_STRENGTH, offset + OFFSET_STORAGE_STRENGTH);
// Compute current R(t)
float ageFraction = DecayStrategy.decay(
DecayStrategy.ageToBucket(timestamp, System.currentTimeMillis()));
// ΔS = S_gain × (1 - R(t)) — maximum boost when retrieval is hard
float deltaS = S_GAIN * (1.0f - ageFraction);
float newS = Math.min(currentS + deltaS, MAX_STORAGE_STRENGTH);
segment.set(LAYOUT_STORAGE_STRENGTH, offset + OFFSET_STORAGE_STRENGTH, newS);
}
}
Calibration Challenges¶
| Parameter | Proposed Default | Notes |
|---|---|---|
| \(S_{\text{gain}}\) | 0.1 | Per-retrieval storage increment |
| \(S_{\text{max}}\) | 5.0 | Cap to prevent runaway storage strength |
| \(\lambda\) | 0.1 | Base decay rate |
| S(t) exponent in scoring | 0.3 | Gentle boost, prevents S domination |
These need empirical calibration with real agent workloads. The key question: how quickly should storage strength accumulate to produce meaningful behavioral differences?
Dependencies & Complexity¶
- Dependencies: V2+ header layout (
storage_strengthfield) ✅ Ready — field exists and is read/written - Complexity: Medium — formula is simple, calibration is the hard part
- Risk: Miscalibrated S_gain can cause "immortal" memories that never decay
Dynamic Quantization Stepping¶
Concept¶
Auto-downgrade vector precision under memory pressure. When off-heap memory usage exceeds a configurable threshold, the system progressively reduces vector quantization from SQ8 (8-bit scalar) to SQ4 (4-bit scalar), trading a small amount of recall accuracy for 2× memory savings.
Biological Basis¶
The brain performs a similar optimization — older memories are stored with less perceptual detail (lower precision) but retain their gist (semantic meaning). You remember that you had a great dinner, but not the exact flavors. The gist is sufficient for retrieval; the sensory detail is pruned.
Quantization Precision Impact¶
| Format | Bits/Dim | Memory/Vector (768d) | Recall@10 Impact |
|---|---|---|---|
| FP32 | 32 | 3,072 bytes | Baseline |
| SQ8 (current) | 8 | 768 bytes | ~0.5% degradation |
| SQ4 (proposed) | 4 | 384 bytes | ~2-3% degradation |
| Binary | 1 | 96 bytes | ~8-12% degradation |
Pressure-Based Stepping¶
flowchart TD
M["Monitor: off-heap usage"] --> C{"Usage > threshold?"}
C -->|"< 70%"| OK["Phase 0: Normal (SQ8)"]
C -->|"70-85%"| P1["Phase 1: SQ4 oldest 25%"]
C -->|"85-95%"| P2["Phase 2: SQ4 all non-pinned"]
C -->|"> 95%"| P3["Phase 3: Deep Sleep + aggressive prune"]
P1 -.- S1["~12% memory saved<br/>~0.5% recall impact"]
P2 -.- S2["~50% memory saved<br/>~2% recall impact"]
P3 -.- S3["Variable savings<br/>memories permanently lost"]
style OK fill:#27ae60,color:white
style P1 fill:#f39c12,color:white
style P2 fill:#e67e22,color:white
style P3 fill:#e74c3c,color:white
SQ4 Encoding¶
SQ4 packs two dimensions into a single byte using 4-bit uniform quantization:
/**
* Encodes two float values into a single byte (4 bits each).
*/
static byte encodeSQ4Pair(float v1, float v2, float min, float scale) {
int q1 = Math.clamp(Math.round((v1 - min) / scale * 15f), 0, 15);
int q2 = Math.clamp(Math.round((v2 - min) / scale * 15f), 0, 15);
return (byte) ((q1 << 4) | q2);
}
/**
* Decodes a byte back to two approximate float values.
*/
static float[] decodeSQ4Pair(byte packed, float min, float scale) {
int q1 = (packed >> 4) & 0x0F;
int q2 = packed & 0x0F;
return new float[]{
min + (q1 / 15f) * scale,
min + (q2 / 15f) * scale
};
}
Online Re-Quantization¶
The critical engineering challenge: re-quantizing vectors without locking the store. The proposed approach:
- Shadow copy: Create a parallel SQ4 segment alongside the existing SQ8 segment
- Background conversion: A background Virtual Thread re-quantizes records in batches of 1,000
- Atomic swap: Once complete, atomically update the
CognitiveRecordLayoutstride to use SQ4 offsets - Lazy cleanup: The old SQ8 bytes become dead space, reclaimed at next compaction
/**
* Re-quantizes a batch of records from SQ8 to SQ4 in-place.
*
* Thread safety: uses compare-and-swap on a "quantization version" byte
* in the header flags to prevent double-conversion.
*/
public int requantizeBatch(MemorySegment segment, int startRecord,
int batchSize, CognitiveRecordLayout layout) {
int converted = 0;
for (int i = startRecord; i < startRecord + batchSize; i++) {
long offset = (long) i * layout.stride();
byte flags = segment.get(LAYOUT_FLAGS, offset + OFFSET_FLAGS);
// Skip pinned, already-SQ4, or tombstoned
if (isPinned(flags) || isSQ4(flags) || isTombstoned(flags)) continue;
// Read SQ8 vector, re-quantize to SQ4
byte[] sq8 = readVector(segment, offset, layout);
byte[] sq4 = convertSQ8toSQ4(sq8);
// Write SQ4 in-place (half the space)
writeVectorSQ4(segment, offset, layout, sq4);
// Mark as SQ4 in flags (atomic CAS)
setQuantizationFlag(segment, offset, QUANT_SQ4);
converted++;
}
return converted;
}
Mixed-Precision Scoring¶
The CognitiveScorer must handle mixed SQ8/SQ4 segments:
// Phase 5: Vector distance — check quantization format per-record
byte flags = segment.get(LAYOUT_FLAGS, offset + OFFSET_FLAGS);
float l2dist;
if (isSQ4(flags)) {
l2dist = SimilarityFunction.EUCLIDEAN.computeSQ4FromSegment(
queryVector, segment, layout.vectorOffset(offset),
effectiveMins, effectiveScales, layout.quantizedVecBytes() / 2);
} else {
l2dist = SimilarityFunction.EUCLIDEAN.computeQuantizedFromSegment(
queryVector, segment, layout.vectorOffset(offset),
effectiveMins, effectiveScales, layout.quantizedVecBytes());
}
Dependencies & Complexity¶
- Dependencies: ReflectDaemon Phase 0 (memory pressure monitoring), ScalarQuantizer SQ4 support (new)
- Complexity: High — online re-quantization without locking, mixed-precision scoring in the hot loop, SIMD kernel for SQ4 distance computation
- Risk: SQ4 distance computation is not yet SIMD-optimized; 4-bit unpacking adds ~30% overhead per distance call until a dedicated SIMD kernel is written
Priority Matrix¶
| Feature | Value | Complexity | Dependencies Ready? | Estimated Effort |
|---|---|---|---|---|
| Two-Factor Memory (R+S) | 🟢 High | Medium | ✅ | 1-2 weeks |
| Executive Dysfunction | 🟡 Medium | Medium | ✅ | 1-2 weeks |
| Neuromodulatory Gain | 🟡 Medium | High | ⏳ | 3-4 weeks |
| Dynamic Quantization | 🟡 Medium | High | ⏳ | 4-6 weeks |
Contributing to Labs¶
Labs features are developed on labs/* branches and are not merged to main until they graduate from experimental status. If you're interested in contributing:
- Check the Contributing Guide
- Open an issue with the
labslabel describing which feature and your proposed approach - Branch from
mainaslabs/feature-name - Labs branches have relaxed test coverage requirements (60% vs 80% for main)
- Features graduate to
mainafter passing a design review + benchmark validation