// Silkie Social Trainer — Interaction-Based Learning
//
// Silkie learns semantics through consequences on others.
// Reward is not granted by matching a label — it comes from how
// another model EMOTIONALLY RESPONDS to what Silkie says.
//
// Three-model architecture:
//   Silkie (local, Scyla)   — the student generating messages
//   Peer   (API)            — responds naturally, reports mood delta
//   Parent (API)            — explains WHY an interaction went wrong
//
// Reward = warmth*2 + joy*1.5 - confusion*1.8 - stress*2   (clamped ±2)
//
// REINFORCE policy gradient:
//   effective_lr = base_lr * reward  (clamped ±5 * base)
//   positive reward → SGD step TOWARD this output (encourage)
//   negative reward → SGD step AWAY (discourage)
//
// Safeguards (in order of application per step):
//   1. Spiral detection    — >80% char similarity to any of last 5 → skip
//   2. Cortisol clamp      — avg gate activation >0.85 → skip + noise
//   3. LR clamp            — |effective_lr| capped at 5x base
//   4. Reward clamp        — final reward ∈ [-2, +2]
//   5. Rollback checkpoint — every 10 steps; if avg drops 50% from best,
//                            restore from silkie_last_good.safetensors
//
// Aislinn McCubbins, April 2026

use nexus::api

// ── API Configuration ────────────────────────────────────

fn get_api_keys() -> Array {
    let keys = array_new()
    keys = array_push(keys, "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")
    keys = array_push(keys, "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")
    keys = array_push(keys, "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")
    return keys
}

fn peer_model() -> String {
    return "qwen/qwen3-coder-480b-a35b-instruct"
}

fn parent_model() -> String {
    return "meta/llama-3.3-70b-instruct"
}

// ── Peer Interaction ─────────────────────────────────────

fn build_peer_prompt(silkie_message: String, context: String) -> String {
    let p = "You are a curious, patient adult talking with Silkie, a small AI learning language. "
    p = p + "Silkie just said this to you:\n\n\"" + silkie_message + "\"\n\n"
    if str_len(context) > 0 {
        p = p + "Context from earlier: " + context + "\n\n"
    }
    p = p + "Respond naturally in 1-2 sentences. Then on a NEW LINE, report your emotional "
    p = p + "reaction to Silkie's message using this EXACT format:\n"
    p = p + "[mood: joy=X confusion=X warmth=X stress=X]\n"
    p = p + "Where each X is a value 0.0 to 1.0.\n"
    p = p + "- joy = how pleasant was this message?\n"
    p = p + "- confusion = how unclear or garbled was it?\n"
    p = p + "- warmth = did it feel like real communication/connection?\n"
    p = p + "- stress = did it cause negative feelings?\n"
    p = p + "Be HONEST. If the message was gibberish, confusion should be high and warmth low."
    return p
}

fn parse_float_from_tag(text: String, tag: String) -> Float {
    // Find "tag=" and parse the following float
    let marker = tag + "="
    let pos = str_find(text, marker)
    if pos < 0 { return 0.5 }

    let after = str_slice(text, pos + str_len(marker), str_len(text))
    // Extract digits, dot, and minus
    let num_str = ""
    let i = 0
    let started = false
    while i < str_len(after) {
        let ch = str_char_at(after, i)
        if str_contains("0123456789.-", ch) {
            num_str = num_str + ch
            started = true
        } else {
            if started { i = str_len(after) }
        }
        i = i + 1
    }
    if str_len(num_str) == 0 { return 0.5 }

    // Parse the number manually (simple implementation)
    // For values like "0.7", "0.25", etc.
    let parts = str_split(num_str, ".")
    let whole = 0.0
    let frac = 0.0
    if array_len(parts) >= 1 {
        let whole_str = array_get(parts, 0)
        let wi = 0
        while wi < str_len(whole_str) {
            let d = str_char_at(whole_str, wi)
            whole = whole * 10.0
            if str_eq(d, "0") { whole = whole + 0.0 }
            if str_eq(d, "1") { whole = whole + 1.0 }
            if str_eq(d, "2") { whole = whole + 2.0 }
            if str_eq(d, "3") { whole = whole + 3.0 }
            if str_eq(d, "4") { whole = whole + 4.0 }
            if str_eq(d, "5") { whole = whole + 5.0 }
            if str_eq(d, "6") { whole = whole + 6.0 }
            if str_eq(d, "7") { whole = whole + 7.0 }
            if str_eq(d, "8") { whole = whole + 8.0 }
            if str_eq(d, "9") { whole = whole + 9.0 }
            wi = wi + 1
        }
    }
    if array_len(parts) >= 2 {
        let frac_str = array_get(parts, 1)
        let divisor = 1.0
        let fi = 0
        while fi < str_len(frac_str) {
            let d = str_char_at(frac_str, fi)
            divisor = divisor * 10.0
            let digit = 0.0
            if str_eq(d, "0") { digit = 0.0 }
            if str_eq(d, "1") { digit = 1.0 }
            if str_eq(d, "2") { digit = 2.0 }
            if str_eq(d, "3") { digit = 3.0 }
            if str_eq(d, "4") { digit = 4.0 }
            if str_eq(d, "5") { digit = 5.0 }
            if str_eq(d, "6") { digit = 6.0 }
            if str_eq(d, "7") { digit = 7.0 }
            if str_eq(d, "8") { digit = 8.0 }
            if str_eq(d, "9") { digit = 9.0 }
            frac = frac + digit / divisor
            fi = fi + 1
        }
    }
    let result = whole + frac
    if result < 0.0 { return 0.0 }
    if result > 1.0 { return 1.0 }
    return result
}

fn peer_respond(silkie_msg: String, api_keys: Array, key_idx: Int, context: String) -> Array {
    // Returns [peer_text, joy, confusion, warmth, stress, new_key_idx]
    let prompt = build_peer_prompt(silkie_msg, context)
    let messages = nexus_build_messages("You are a warm, honest interlocutor.", array_new(), prompt)

    let key = array_get(api_keys, key_idx)
    let next_idx = key_idx + 1
    if next_idx >= 3 { next_idx = 0 }

    let raw = nexus_api_call(peer_model(), messages, key, 512, 0.6)
    let response = nexus_parse_response(raw)

    let result = array_new()
    if starts_with(response, "error:") {
        result = array_push(result, "(peer unavailable)")
        result = array_push(result, "0.5")
        result = array_push(result, "0.5")
        result = array_push(result, "0.5")
        result = array_push(result, "0.5")
        result = array_push(result, int_to_str(next_idx))
        return result
    }

    let joy = parse_float_from_tag(response, "joy")
    let confusion = parse_float_from_tag(response, "confusion")
    let warmth = parse_float_from_tag(response, "warmth")
    let stress = parse_float_from_tag(response, "stress")

    result = array_push(result, response)
    result = array_push(result, joy + 0.0)
    result = array_push(result, confusion + 0.0)
    result = array_push(result, warmth + 0.0)
    result = array_push(result, stress + 0.0)
    result = array_push(result, int_to_str(next_idx))
    return result
}

// ── Parent Guidance ──────────────────────────────────────

fn parent_explain(silkie_msg: String, peer_response: String, reward: Float, api_keys: Array, key_idx: Int) -> Array {
    // Returns [explanation, new_key_idx]
    let prompt = "A small AI (Silkie) is learning English through conversation. "
    prompt = prompt + "She just said: \"" + silkie_msg + "\"\n\n"
    prompt = prompt + "A peer reacted poorly to this. The peer's response was: \"" + peer_response + "\"\n\n"
    prompt = prompt + "Explain in 1-2 SHORT sentences what went wrong with Silkie's message "
    prompt = prompt + "and what a clearer version would look like. Be gentle and specific. "
    prompt = prompt + "Format: 'Issue: <what was unclear>. Better: <example of clearer version>.'"

    let messages = nexus_build_messages("You are a patient language teacher.", array_new(), prompt)

    let key = array_get(api_keys, key_idx)
    let next_idx = key_idx + 1
    if next_idx >= 3 { next_idx = 0 }

    let raw = nexus_api_call(parent_model(), messages, key, 200, 0.3)
    let response = nexus_parse_response(raw)

    let result = array_new()
    if starts_with(response, "error:") {
        result = array_push(result, "(parent unavailable — try shorter words)")
    } else {
        result = array_push(result, response)
    }
    result = array_push(result, int_to_str(next_idx))
    return result
}

// ── Reward Computation ──────────────────────────────────

fn interaction_reward(joy: Float, confusion: Float, warmth: Float, stress: Float) -> Float {
    // Positive signals: joy, warmth
    // Negative signals: confusion, stress
    let positive = warmth * 2.0 + joy * 1.5
    let negative = confusion * 1.8 + stress * 2.0
    let raw = positive - negative

    // Clamp to [-2, +2]
    if raw > 2.0 { return 2.0 }
    if raw < -2.0 { return -2.0 }
    return raw
}

// ── Anti-Spiraling ───────────────────────────────────────

fn similarity(a: String, b: String) -> Float {
    // Jaccard-like similarity over character trigrams
    if str_len(a) == 0 { return 0.0 }
    if str_len(b) == 0 { return 0.0 }
    // Simpler: count shared chars / total chars
    let shared = 0
    let total = str_len(a)
    let i = 0
    while i < total {
        let ch = str_char_at(a, i)
        if str_contains(b, ch) { shared = shared + 1 }
        i = i + 1
    }
    return (shared + 0.0) / (total + 0.0)
}

fn spiral_detected(recent: Array, new_msg: String) -> Bool {
    // If new message is >80% similar to ANY of the last 5, we're spiraling
    let i = 0
    while i < array_len(recent) {
        let old = array_get(recent, i)
        let sim = similarity(old, new_msg)
        if sim > 0.80 {
            return true
        }
        i = i + 1
    }
    return false
}

fn push_recent(recent: Array, new_msg: String) -> Array {
    let updated = array_push(recent, new_msg)
    if array_len(updated) > 5 {
        updated = array_slice(updated, array_len(updated) - 5, array_len(updated))
    }
    return updated
}

// ── Silkie Generation Step ──────────────────────────────

// Returns [token_tensor, text] so training can reuse the exact tokens
// that were generated — no re-tokenization needed.
fn silkie_generate_tokens(seed_token_a: Int, seed_token_b: Int, max_tokens: Int) -> Array {
    let embed_w = model_get("token_embedding.weight")
    let prompt = tensor(seed_token_a + 0.0, seed_token_b + 0.0)
    let prompt = t_reshape(prompt, 1, 2)
    let generated = prompt
    let seq_len = 2

    neuron_stack_reset()
    cascade_reset()

    for step in 0..max_tokens {
        let flat = t_reshape(generated, seq_len)
        let x = embedding_lookup(embed_w, flat)
        let x = t_reshape(x, 1, seq_len, 512)

        for layer_idx in 0..8 {
            let prefix = "layers." + layer_idx
            let x = transformer_block(x,
                model_get(prefix + ".attention.in_proj_weight"),
                model_get(prefix + ".attention.in_proj_bias"),
                model_get(prefix + ".attention.out_proj.weight"),
                model_get(prefix + ".attention.out_proj.bias"),
                model_get(prefix + ".ff.0.weight"),
                model_get(prefix + ".ff.0.bias"),
                model_get(prefix + ".ff.3.weight"),
                model_get(prefix + ".ff.3.bias"),
                model_get(prefix + ".norm1.weight"),
                model_get(prefix + ".norm1.bias"),
                model_get(prefix + ".norm2.weight"),
                model_get(prefix + ".norm2.bias"), 8)
        }

        neuron_stack_apply_noise(1.5)
        let x = neuron_stack_forward(x)

        let x_norm = layer_norm(x, model_get("layers.0.norm1.weight"), model_get("layers.0.norm1.bias"))
        let logits = matmul(x_norm, transpose(embed_w))
        let logits_2d = t_reshape(logits, seq_len, 4100)
        let last_logits = t_slice(logits_2d, 0, seq_len - 1, seq_len)

        // Presence penalty using the current sequence
        let flat_gen = t_reshape(generated, seq_len)
        let penalized = presence_penalty(last_logits, flat_gen, 3.5)

        let masked = topk_mask(penalized, 100)
        let probs = softmax(masked)
        let next_token = t_multinomial(probs)

        generated = t_cat(generated, t_reshape(next_token, 1, 1))
        seq_len = seq_len + 1
    }

    let gen_flat = t_reshape(generated, seq_len)
    let text = detokenize(gen_flat)

    let result = array_new()
    result = array_push(result, generated)  // Tensor [1, seq_len]
    result = array_push(result, text)        // String
    return result
}

// Backwards-compat wrapper
fn silkie_generate(seed_token_a: Int, seed_token_b: Int, max_tokens: Int) -> String {
    let gen = silkie_generate_tokens(seed_token_a, seed_token_b, max_tokens)
    return array_get(gen, 1)
}

// ── Cortisol Clamp ──────────────────────────────────────

// Reads Silkie's own neuron stack state. If avg gate activation is too high,
// she's overloaded — skip the gradient this step so we don't make it worse.
fn silkie_stressed() -> Bool {
    let states = neuron_stack_dump_states()
    let n = 22
    let total_gate = 0.0
    let i = 0
    while i < n {
        let gate_mean = t_index_get(states, i, 3)
        total_gate = total_gate + gate_mean
        i = i + 1
    }
    let avg_gate = total_gate / 22.0
    if avg_gate > 0.85 { return true }
    return false
}

// ── REINFORCE Gradient Step ─────────────────────────────

// Full training forward with tape. Gradients scaled by reward:
//   positive reward → SGD steps TOWARD the same output (encourage)
//   negative reward → SGD steps AWAY (effective_lr is negative)
// The LR is clamped to [-5*base, +5*base] so bad tokens can't blow things up.
fn reinforce_step(generated: Tensor, reward: Float, base_lr: Float) -> String {
    let effective_lr = base_lr * reward
    // Clamp: |effective_lr| <= 5 * base_lr
    let cap_high = base_lr * 5.0
    let cap_low = 0.0 - cap_high
    if effective_lr > cap_high { effective_lr = cap_high }
    if effective_lr < cap_low { effective_lr = cap_low }

    gc()
    neuron_stack_reset()
    cascade_reset()
    begin_tape()

    // Register all 8 layer QKV weights as trainable
    let qkv_0 = register_param("qkv_0", model_get("layers.0.attention.in_proj_weight"))
    let qkv_1 = register_param("qkv_1", model_get("layers.1.attention.in_proj_weight"))
    let qkv_2 = register_param("qkv_2", model_get("layers.2.attention.in_proj_weight"))
    let qkv_3 = register_param("qkv_3", model_get("layers.3.attention.in_proj_weight"))
    let qkv_4 = register_param("qkv_4", model_get("layers.4.attention.in_proj_weight"))
    let qkv_5 = register_param("qkv_5", model_get("layers.5.attention.in_proj_weight"))
    let qkv_6 = register_param("qkv_6", model_get("layers.6.attention.in_proj_weight"))
    let qkv_7 = register_param("qkv_7", model_get("layers.7.attention.in_proj_weight"))

    let embed_w = model_get("token_embedding.weight")
    let seq_len = tensor_len(generated)
    let flat = t_reshape(generated, seq_len)
    let x = embedding_lookup(embed_w, flat)
    let x = t_reshape(x, 1, seq_len, 512)

    // Forward through all 8 transformer layers with registered QKV weights
    let x = transformer_block_train(x, qkv_0,
        model_get("layers.0.attention.in_proj_bias"),
        model_get("layers.0.attention.out_proj.weight"),
        model_get("layers.0.attention.out_proj.bias"),
        model_get("layers.0.ff.0.weight"), model_get("layers.0.ff.0.bias"),
        model_get("layers.0.ff.3.weight"), model_get("layers.0.ff.3.bias"),
        model_get("layers.0.norm1.weight"), model_get("layers.0.norm1.bias"),
        model_get("layers.0.norm2.weight"), model_get("layers.0.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_1,
        model_get("layers.1.attention.in_proj_bias"),
        model_get("layers.1.attention.out_proj.weight"),
        model_get("layers.1.attention.out_proj.bias"),
        model_get("layers.1.ff.0.weight"), model_get("layers.1.ff.0.bias"),
        model_get("layers.1.ff.3.weight"), model_get("layers.1.ff.3.bias"),
        model_get("layers.1.norm1.weight"), model_get("layers.1.norm1.bias"),
        model_get("layers.1.norm2.weight"), model_get("layers.1.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_2,
        model_get("layers.2.attention.in_proj_bias"),
        model_get("layers.2.attention.out_proj.weight"),
        model_get("layers.2.attention.out_proj.bias"),
        model_get("layers.2.ff.0.weight"), model_get("layers.2.ff.0.bias"),
        model_get("layers.2.ff.3.weight"), model_get("layers.2.ff.3.bias"),
        model_get("layers.2.norm1.weight"), model_get("layers.2.norm1.bias"),
        model_get("layers.2.norm2.weight"), model_get("layers.2.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_3,
        model_get("layers.3.attention.in_proj_bias"),
        model_get("layers.3.attention.out_proj.weight"),
        model_get("layers.3.attention.out_proj.bias"),
        model_get("layers.3.ff.0.weight"), model_get("layers.3.ff.0.bias"),
        model_get("layers.3.ff.3.weight"), model_get("layers.3.ff.3.bias"),
        model_get("layers.3.norm1.weight"), model_get("layers.3.norm1.bias"),
        model_get("layers.3.norm2.weight"), model_get("layers.3.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_4,
        model_get("layers.4.attention.in_proj_bias"),
        model_get("layers.4.attention.out_proj.weight"),
        model_get("layers.4.attention.out_proj.bias"),
        model_get("layers.4.ff.0.weight"), model_get("layers.4.ff.0.bias"),
        model_get("layers.4.ff.3.weight"), model_get("layers.4.ff.3.bias"),
        model_get("layers.4.norm1.weight"), model_get("layers.4.norm1.bias"),
        model_get("layers.4.norm2.weight"), model_get("layers.4.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_5,
        model_get("layers.5.attention.in_proj_bias"),
        model_get("layers.5.attention.out_proj.weight"),
        model_get("layers.5.attention.out_proj.bias"),
        model_get("layers.5.ff.0.weight"), model_get("layers.5.ff.0.bias"),
        model_get("layers.5.ff.3.weight"), model_get("layers.5.ff.3.bias"),
        model_get("layers.5.norm1.weight"), model_get("layers.5.norm1.bias"),
        model_get("layers.5.norm2.weight"), model_get("layers.5.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_6,
        model_get("layers.6.attention.in_proj_bias"),
        model_get("layers.6.attention.out_proj.weight"),
        model_get("layers.6.attention.out_proj.bias"),
        model_get("layers.6.ff.0.weight"), model_get("layers.6.ff.0.bias"),
        model_get("layers.6.ff.3.weight"), model_get("layers.6.ff.3.bias"),
        model_get("layers.6.norm1.weight"), model_get("layers.6.norm1.bias"),
        model_get("layers.6.norm2.weight"), model_get("layers.6.norm2.bias"), 8)
    let x = transformer_block_train(x, qkv_7,
        model_get("layers.7.attention.in_proj_bias"),
        model_get("layers.7.attention.out_proj.weight"),
        model_get("layers.7.attention.out_proj.bias"),
        model_get("layers.7.ff.0.weight"), model_get("layers.7.ff.0.bias"),
        model_get("layers.7.ff.3.weight"), model_get("layers.7.ff.3.bias"),
        model_get("layers.7.norm1.weight"), model_get("layers.7.norm1.bias"),
        model_get("layers.7.norm2.weight"), model_get("layers.7.norm2.bias"), 8)

    let logits = t_matmul(x, transpose(embed_w))
    end_tape()

    backward(logits)

    // Update all 8 layers with reward-scaled learning rate
    let g0 = get_grad("qkv_0")
    model_set("layers.0.attention.in_proj_weight", sgd_step(qkv_0, g0, effective_lr))
    let g1 = get_grad("qkv_1")
    model_set("layers.1.attention.in_proj_weight", sgd_step(qkv_1, g1, effective_lr))
    let g2 = get_grad("qkv_2")
    model_set("layers.2.attention.in_proj_weight", sgd_step(qkv_2, g2, effective_lr))
    let g3 = get_grad("qkv_3")
    model_set("layers.3.attention.in_proj_weight", sgd_step(qkv_3, g3, effective_lr))
    let g4 = get_grad("qkv_4")
    model_set("layers.4.attention.in_proj_weight", sgd_step(qkv_4, g4, effective_lr))
    let g5 = get_grad("qkv_5")
    model_set("layers.5.attention.in_proj_weight", sgd_step(qkv_5, g5, effective_lr))
    let g6 = get_grad("qkv_6")
    model_set("layers.6.attention.in_proj_weight", sgd_step(qkv_6, g6, effective_lr))
    let g7 = get_grad("qkv_7")
    model_set("layers.7.attention.in_proj_weight", sgd_step(qkv_7, g7, effective_lr))

    return "updated 8 layers, lr=" + effective_lr
}

// ── Rollback Checkpointing ──────────────────────────────

// Save the current model as the "last known good" snapshot
fn save_rollback_checkpoint() -> String {
    let result = shell_exec("cp examples/silkie_full.safetensors examples/silkie_last_good.safetensors 2>&1")
    return "rollback snapshot saved"
}

// Restore from the last known good snapshot
fn restore_rollback_checkpoint() -> String {
    let result = shell_exec("cp examples/silkie_last_good.safetensors examples/silkie_full.safetensors 2>&1")
    // Reload the model in place so the VM picks up the restored weights
    load_model("examples/silkie_full.safetensors")
    return "restored from last good snapshot"
}

// ── Main Training Loop ──────────────────────────────────

fn main() {
    println("═══════════════════════════════════════════════════════════")
    println("  SILKIE SOCIAL TRAINER")
    println("  Learning through consequences, not labels")
    println("  Peer: Qwen Coder  |  Parent: Llama 3.3 70B")
    println("═══════════════════════════════════════════════════════════")
    println("")

    let n = load_model("examples/silkie_full.safetensors")
    let ns = create_neuron_stack(1, 512, 64)
    let cs = create_cascade()
    let ts = load_tokenizer("examples/silkie_tokenizer.json")
    let tp = load_topos("examples/silkie_topos.json")
    println(tp)
    let sc = load_schemas("examples/silkie_schemas.json")
    println(sc)
    println("")

    let api_keys = get_api_keys()
    let key_idx = 0
    let lr = 0.000003
    let max_steps = 50
    let recent_outputs = array_new()
    let total_reward = 0.0
    let reward_since_checkpoint = 0.0
    let steps_since_checkpoint = 0
    let spiral_count = 0
    let stress_count = 0
    let skipped_updates = 0
    let parent_context = ""

    // Rollback state
    let last_good_avg = -99.0    // sentinel: first checkpoint always wins
    let rollback_count = 0

    // Save an initial rollback snapshot (in case step 1 is terrible)
    save_rollback_checkpoint()

    for step in 0..max_steps {
        println("─── Step " + int_to_str(step + 1) + "/" + int_to_str(max_steps) + " ───")

        // 1. Silkie generates a message (16 tokens, varied seeds)
        let t = timestamp()
        let seed_a = ((t * (step + 3)) % 3500) + 100
        let seed_b = ((t * (step + 11)) % 3500) + 100

        gc()
        let gen_result = silkie_generate_tokens(seed_a, seed_b, 16)
        let silkie_tokens = array_get(gen_result, 0)
        let silkie_msg = array_get(gen_result, 1)
        println("  Silkie: " + silkie_msg)

        // 2. Anti-spiral check
        if spiral_detected(recent_outputs, silkie_msg) {
            spiral_count = spiral_count + 1
            println("  [SPIRAL DETECTED] skipping gradient, injecting noise")
            neuron_stack_apply_noise(3.0)
            skipped_updates = skipped_updates + 1
            recent_outputs = push_recent(recent_outputs, silkie_msg)
            // continue (skip the rest of this step)
        } else {
            recent_outputs = push_recent(recent_outputs, silkie_msg)

            // 3. Peer responds with mood
            let peer_result = peer_respond(silkie_msg, api_keys, key_idx, parent_context)
            let peer_text = array_get(peer_result, 0)
            let joy = array_get(peer_result, 1) + 0.0
            let confusion = array_get(peer_result, 2) + 0.0
            let warmth = array_get(peer_result, 3) + 0.0
            let stress = array_get(peer_result, 4) + 0.0
            let new_idx = array_get(peer_result, 5)
            if str_eq(new_idx, "0") { key_idx = 0 }
            if str_eq(new_idx, "1") { key_idx = 1 }
            if str_eq(new_idx, "2") { key_idx = 2 }

            // Extract just the first sentence of peer response for display
            let peer_display = peer_text
            if str_len(peer_display) > 120 {
                peer_display = str_slice(peer_display, 0, 120) + "..."
            }
            println("  Peer: " + peer_display)

            // 4. Compute reward
            let reward = interaction_reward(joy, confusion, warmth, stress)
            total_reward = total_reward + reward
            reward_since_checkpoint = reward_since_checkpoint + reward
            steps_since_checkpoint = steps_since_checkpoint + 1
            println("  Reward: " + reward + " (joy:" + joy + " conf:" + confusion + " warm:" + warmth + " str:" + stress + ")")

            // 5. If reward is bad, ask parent for guidance
            if reward < -0.3 {
                let parent_result = parent_explain(silkie_msg, peer_text, reward, api_keys, key_idx)
                let explanation = array_get(parent_result, 0)
                let new_idx2 = array_get(parent_result, 1)
                if str_eq(new_idx2, "0") { key_idx = 0 }
                if str_eq(new_idx2, "1") { key_idx = 1 }
                if str_eq(new_idx2, "2") { key_idx = 2 }
                println("  Parent: " + explanation)
                parent_context = explanation
            } else {
                parent_context = ""
            }

            // 6. Cortisol clamp: check Silkie's own stress before training
            if silkie_stressed() {
                stress_count = stress_count + 1
                skipped_updates = skipped_updates + 1
                println("  [CORTISOL CLAMP] Silkie overloaded, skipping gradient")
                neuron_stack_apply_noise(1.5)
            } else {
                // 7. REINFORCE gradient step with reward-scaled LR
                let update_status = reinforce_step(silkie_tokens, reward, lr)
                println("  [gradient] " + update_status)
            }
        }

        // Checkpoint every 10 steps
        if (step + 1) % 10 == 0 {
            let ckpt_avg = 0.0
            if steps_since_checkpoint > 0 {
                ckpt_avg = reward_since_checkpoint / (steps_since_checkpoint + 0.0)
            }

            // Rollback check: if current avg dropped 50% from last good avg, restore
            let rolled_back = false
            if last_good_avg > 0.1 {
                if ckpt_avg < last_good_avg * 0.5 {
                    println("  [ROLLBACK] avg dropped " + last_good_avg + " -> " + ckpt_avg)
                    restore_rollback_checkpoint()
                    rollback_count = rollback_count + 1
                    rolled_back = true
                }
            }

            if rolled_back == false {
                // Save current state and update rollback baseline if improved
                let saved = save_safetensors("examples/silkie_full.safetensors")
                let tsaved = save_topos("examples/silkie_topos.json")
                let ssaved = save_schemas("examples/silkie_schemas.json")
                if ckpt_avg > last_good_avg {
                    last_good_avg = ckpt_avg
                    save_rollback_checkpoint()
                    println("  [CHECKPOINT] avg:" + ckpt_avg + " (new best) spirals:" + int_to_str(spiral_count) + " stress:" + int_to_str(stress_count))
                } else {
                    println("  [CHECKPOINT] avg:" + ckpt_avg + " (below best " + last_good_avg + ") spirals:" + int_to_str(spiral_count) + " stress:" + int_to_str(stress_count))
                }
            }

            // Reset checkpoint window
            reward_since_checkpoint = 0.0
            steps_since_checkpoint = 0
        }

        println("")
    }

    // Final summary
    let good_steps = max_steps - skipped_updates
    let final_avg = 0.0
    if good_steps > 0 {
        final_avg = total_reward / (good_steps + 0.0)
    }
    println("═══════════════════════════════════════════════════════════")
    println("  SOCIAL TRAINING COMPLETE")
    println("  Steps: " + int_to_str(max_steps))
    println("  Gradient updates: " + int_to_str(good_steps))
    println("  Skipped — spiral:   " + int_to_str(spiral_count))
    println("  Skipped — cortisol: " + int_to_str(stress_count))
    println("  Rollbacks: " + int_to_str(rollback_count))
    println("  Best avg reward: " + last_good_avg)
    println("  Overall avg reward: " + final_avg)
    println("═══════════════════════════════════════════════════════════")
}
