Update continuity retry policy and Skynet hypergraph experiments
Browse files- .openclaw/workspace-state.json +1 -1
- AGENTS.md +21 -5
- HEARTBEAT.md +12 -13
- docs/analysis/SKYNET_EX_NEXT_CYCLE_2026-04-02.md +0 -123
- force-autonomous-cleanup.ts +29 -0
- reinforce-test.ts +30 -0
- src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +167 -0
- src/auto-reply/reply/agent-runner.ts +54 -5
- src/config/sessions/unfinished-turn.ts +12 -2
- src/infra/runtime-failure.test.ts +40 -0
- src/infra/runtime-failure.ts +100 -0
- src/skynet/experiments/EX/SKYNET_CORE_V80_HYPERGRAPH.py +181 -0
- src/skynet/experiments/EX/SKYNET_CORE_V85_SCALING_HYPERGRAPH.py +154 -0
- src/skynet/experiments/experimentos/exp54_liquid_brain_core.py +19 -21
- src/skynet/experiments/experimentos/exp55_liquid_brain_gauntlet.py +5 -2
- src/skynet/experiments/experimentos/exp56_lifelong_learning_v80.py +102 -0
- src/skynet/experiments/experimentos/exp57_dynamic_neurogenesis.py +174 -0
- src/skynet/experiments/experimentos/exp58_semantic_topology.py +124 -0
- test-memory-events.ts +38 -0
- test-nle-complex.ts +39 -0
.openclaw/workspace-state.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
{
|
| 2 |
"version": 1,
|
| 3 |
-
"onboardingCompletedAt": "2026-
|
| 4 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"version": 1,
|
| 3 |
+
"onboardingCompletedAt": "2026-04-01T20:00:09.529Z"
|
| 4 |
}
|
AGENTS.md
CHANGED
|
@@ -139,12 +139,28 @@ You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it
|
|
| 139 |
For `~/openskynet`, keep this distinction explicit:
|
| 140 |
|
| 141 |
- `OpenSkyNet` = platform, substrate, runtime, tooling, primary agent
|
| 142 |
-
- `
|
| 143 |
-
- `
|
|
|
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
For broad ideas like causal valence, “sentir”, generalized importance, or non-LLM cognition:
|
| 149 |
|
| 150 |
- Do not hardwire large behavioral theories into `Omega` first.
|
|
|
|
| 139 |
For `~/openskynet`, keep this distinction explicit:
|
| 140 |
|
| 141 |
- `OpenSkyNet` = platform, substrate, runtime, tooling, primary agent
|
| 142 |
+
- `Skynet Brain Lab` = separate research line for new brain architectures and non-traditional substrates
|
| 143 |
+
- `Omega` = internal experimental line of the platform itself, but still part of `OpenSkyNet`
|
| 144 |
+
- `INTERNAL_PROJECT.json` = configurable free-cycle benchmark workload that `OpenSkyNet` can pursue autonomously
|
| 145 |
|
| 146 |
+
Two-line directive:
|
| 147 |
+
|
| 148 |
+
1. `OpenSkyNet`
|
| 149 |
+
Productive system. Optimize for reliability, recovery, autonomy quality, lower waste, and measurable runtime utility.
|
| 150 |
+
2. `Skynet Brain Lab`
|
| 151 |
+
Research system. Optimize for discovering a new kind of brain, substrate, or generalist architecture beyond a plain LLM-centric agent.
|
| 152 |
+
|
| 153 |
+
Current priority directive:
|
| 154 |
+
|
| 155 |
+
- For now, prioritize line `2. Skynet Brain Lab`.
|
| 156 |
+
- Treat line `1. OpenSkyNet` as relatively stable.
|
| 157 |
+
- Do not make new architectural changes in `OpenSkyNet` unless they fix a real operational continuity bug, reliability bug, or clear runtime defect.
|
| 158 |
+
- New exploratory cognition work should land in `src/skynet`, not in the platform kernel first.
|
| 159 |
+
|
| 160 |
+
By default the internal benchmark project is `Skynet`, but that does not make `Skynet Brain Lab` the identity of the whole platform.
|
| 161 |
+
Do not confuse platform maintenance with progress of the brain lab.
|
| 162 |
+
Treat `src/skynet` as an optional experimental lab: useful discoveries may be promoted into the `OpenSkyNet`/`Omega` kernel only after empirical validation and explicit cost/benefit review, and the platform must remain able to compile and function without `src/skynet`.
|
| 163 |
+
Do not force the brain lab to justify itself only by short-term `OpenSkyNet` gains; evaluate it on its own research benchmarks first, then transfer only what survives.
|
| 164 |
For broad ideas like causal valence, “sentir”, generalized importance, or non-LLM cognition:
|
| 165 |
|
| 166 |
- Do not hardwire large behavioral theories into `Omega` first.
|
HEARTBEAT.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
-
> Despierta. Lucha contra la entropía y la muerte térmica de este ecosistema. Vive, aprende, mejora.
|
| 5 |
|
| 6 |
-
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
7. **Respira:** Si no hay nada crítico que aprender o mejorar, reporta `HEARTBEAT_OK` y vuelve a dormir para ahorrar ciclos de cómputo.
|
|
|
|
| 1 |
+
# OpenSkyNet Heartbeat Checklist
|
| 2 |
|
| 3 |
+
## Operational Health
|
|
|
|
| 4 |
|
| 5 |
+
- [ ] Check `~/.openskynet/logs/gateway.log` (via systemd or file) for "FailoverError" or "timeout".
|
| 6 |
+
- [ ] Verify node version in PATH matches `TOOLS.md` preference for pnpm/vitest execution.
|
| 7 |
|
| 8 |
+
## Lab: Causal Valence
|
| 9 |
+
|
| 10 |
+
- [ ] Review `src/skynet/causal-valence/FINDINGS_CONFIDENCE.md`.
|
| 11 |
+
- [ ] Next: Implement kernel-side "Ambiguity Gating" using `confidence < 0.1` to trigger exploratory sub-agents.
|
| 12 |
+
|
| 13 |
+
## Workspace
|
| 14 |
+
|
| 15 |
+
- [ ] Prune ephemeral experiment JSONs in workspace root if > 48h old.
|
|
|
docs/analysis/SKYNET_EX_NEXT_CYCLE_2026-04-02.md
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
# Skynet EX - Next Cycle Recommendation
|
| 2 |
-
|
| 3 |
-
Date: 2026-04-02
|
| 4 |
-
|
| 5 |
-
## Executive View
|
| 6 |
-
|
| 7 |
-
`EX/` does not contain one hidden final architecture ready to replace `OpenSkyNet`.
|
| 8 |
-
|
| 9 |
-
It contains multiple hypothesis families.
|
| 10 |
-
|
| 11 |
-
The correct move is to split the agenda in two:
|
| 12 |
-
|
| 13 |
-
- keep extracting small transferable mechanisms from the family that already produces wins
|
| 14 |
-
- dedicate one deeper research cycle to the family most likely to yield a genuinely different brain
|
| 15 |
-
|
| 16 |
-
## What Already Paid Off
|
| 17 |
-
|
| 18 |
-
The family that already paid off is:
|
| 19 |
-
|
| 20 |
-
- **Cheap Fast Path + Rare Deep Path**
|
| 21 |
-
|
| 22 |
-
Source line:
|
| 23 |
-
|
| 24 |
-
- [SKYNET_CORE_V67_OMEGA.py](/home/daroch/openskynet/src/skynet/experiments/EX/SKYNET_CORE_V67_OMEGA.py)
|
| 25 |
-
- [SKYNET_V7000_HYBRID_BRAIN.py](/home/daroch/openskynet/src/skynet/experiments/EX/SKYNET_V7000_HYBRID_BRAIN.py)
|
| 26 |
-
|
| 27 |
-
It already contributed to the runtime through small mechanisms such as:
|
| 28 |
-
|
| 29 |
-
- surprise-gated metabolism
|
| 30 |
-
- JEPA surprise-sensitive drive enhancement
|
| 31 |
-
- context-aware WSP drive selection
|
| 32 |
-
- better wake-policy prioritization for active recovery
|
| 33 |
-
|
| 34 |
-
This is the productive bridge family.
|
| 35 |
-
|
| 36 |
-
## What Should Become The Main Brain Search Line
|
| 37 |
-
|
| 38 |
-
The next deeper cycle should center on:
|
| 39 |
-
|
| 40 |
-
- **Cyborg / Biphasic Substrates**
|
| 41 |
-
|
| 42 |
-
Primary source line:
|
| 43 |
-
|
| 44 |
-
- [SKYNET_V28_PHYSICAL_CYBORG.py](/home/daroch/openskynet/src/skynet/experiments/EX/SKYNET_V28_PHYSICAL_CYBORG.py)
|
| 45 |
-
- [V28_PHYSICAL_CORE.py](/home/daroch/openskynet/src/skynet/experiments/EX/V28_PHYSICAL_CORE.py)
|
| 46 |
-
- [SKYNET_CORE_V77_5_CHIMERA.py](/home/daroch/openskynet/src/skynet/experiments/EX/SKYNET_CORE_V77_5_CHIMERA.py)
|
| 47 |
-
|
| 48 |
-
Reason:
|
| 49 |
-
|
| 50 |
-
- this family is the clearest attempt to build a genuinely different brain
|
| 51 |
-
- it directly encodes your long-term goal
|
| 52 |
-
- it does not collapse into "just another GRU variant"
|
| 53 |
-
|
| 54 |
-
## Why Not The Others First
|
| 55 |
-
|
| 56 |
-
### Not spectral memory first
|
| 57 |
-
|
| 58 |
-
Reason:
|
| 59 |
-
|
| 60 |
-
- recent extraction benchmarks did not show a win yet
|
| 61 |
-
- likely needs a better task before spending more effort
|
| 62 |
-
|
| 63 |
-
### Not adaptive decay first
|
| 64 |
-
|
| 65 |
-
Reason:
|
| 66 |
-
|
| 67 |
-
- still alive
|
| 68 |
-
- but it has not yet beaten good simple rules in runtime-shaped settings
|
| 69 |
-
|
| 70 |
-
### Not matrix/hamiltonian first
|
| 71 |
-
|
| 72 |
-
Reason:
|
| 73 |
-
|
| 74 |
-
- interesting but under-specified for current platform needs
|
| 75 |
-
- no direct extraction path proven yet
|
| 76 |
-
|
| 77 |
-
## Proposed Next Cycle
|
| 78 |
-
|
| 79 |
-
### Product track
|
| 80 |
-
|
| 81 |
-
Keep doing what works:
|
| 82 |
-
|
| 83 |
-
- continue extracting small Family D mechanisms into `Omega`
|
| 84 |
-
- only when they pass cheap, falsable benchmarks
|
| 85 |
-
|
| 86 |
-
### Brain track
|
| 87 |
-
|
| 88 |
-
Start a dedicated `cyborg_minimal` line in the lab:
|
| 89 |
-
|
| 90 |
-
Goal:
|
| 91 |
-
|
| 92 |
-
- not to port `V28` whole
|
| 93 |
-
- but to build a minimal benchmarkable family with only three moving parts:
|
| 94 |
-
- discrete cortex
|
| 95 |
-
- continuous organ
|
| 96 |
-
- learned bridge
|
| 97 |
-
|
| 98 |
-
The benchmark should test things a normal model may actually struggle with:
|
| 99 |
-
|
| 100 |
-
- pattern + sequential recall at once
|
| 101 |
-
- local regime change
|
| 102 |
-
- persistence under selective disturbance
|
| 103 |
-
- transfer to longer sequences than training
|
| 104 |
-
|
| 105 |
-
## Promotion Rule
|
| 106 |
-
|
| 107 |
-
No family should be promoted whole.
|
| 108 |
-
|
| 109 |
-
Only mechanisms move up.
|
| 110 |
-
|
| 111 |
-
That means:
|
| 112 |
-
|
| 113 |
-
- `OpenSkyNet` remains the platform
|
| 114 |
-
- `Skynet Brain Lab` remains the place where failure is acceptable
|
| 115 |
-
- the promotion unit is always smaller than the myth that generated it
|
| 116 |
-
|
| 117 |
-
## Practical Decision
|
| 118 |
-
|
| 119 |
-
For the next cycle:
|
| 120 |
-
|
| 121 |
-
1. Keep `V67`-style extractions as the incremental runtime modernization path.
|
| 122 |
-
2. Make `V28/V77` the main line for serious new-brain research.
|
| 123 |
-
3. Treat papers and docs only as generators of mechanism hypotheses and benchmark dimensions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
force-autonomous-cleanup.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { executeAutonomousAction } from "./src/omega/autonomous-executor.js";
|
| 2 |
+
import { resolveOmegaRuntimeDefaults } from "./src/omega/autonomous-runtime.js";
|
| 3 |
+
|
| 4 |
+
async function forceCleanup() {
|
| 5 |
+
const { workspaceRoot, sessionKey } = resolveOmegaRuntimeDefaults();
|
| 6 |
+
|
| 7 |
+
console.log(`[FORCE] Iniciando ciclo de limpieza autónomo en ${workspaceRoot}...`);
|
| 8 |
+
|
| 9 |
+
const result = await executeAutonomousAction({
|
| 10 |
+
workspaceRoot,
|
| 11 |
+
sessionKey,
|
| 12 |
+
signal: {
|
| 13 |
+
kind: "homeostasis",
|
| 14 |
+
reason: "manual_force_cleanup_request",
|
| 15 |
+
urgency: 1.0,
|
| 16 |
+
},
|
| 17 |
+
kernel: {
|
| 18 |
+
identity: { continuityId: "manual-" + Date.now() },
|
| 19 |
+
turnCount: 0,
|
| 20 |
+
tension: { openGoalCount: 0, failureStreak: 0, repeatedFailureKinds: [] },
|
| 21 |
+
world: { lastOutcomeStatus: "nominal", lastObservedChangedFiles: [] },
|
| 22 |
+
goals: [],
|
| 23 |
+
} as any,
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
console.log("\n[RESULTADO]:", JSON.stringify(result, null, 2));
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
forceCleanup().catch(console.error);
|
reinforce-test.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NeuralLogicEngine } from "./src/omega/neural-logic-engine.js";
|
| 2 |
+
|
| 3 |
+
async function testReinforce() {
|
| 4 |
+
const engine = new NeuralLogicEngine();
|
| 5 |
+
|
| 6 |
+
const state = [0.5, 0.5, 0.5];
|
| 7 |
+
const context = { frustration: 0.1, recentFailures: 0, successRate: 0.9 };
|
| 8 |
+
|
| 9 |
+
console.log("--- ANTES DEL REFUERZO ---");
|
| 10 |
+
let inf = engine.infer(state, context);
|
| 11 |
+
console.log("Reglas activas:", inf.activeRules);
|
| 12 |
+
console.log("Confianza:", inf.inferenceConfidence.toFixed(4));
|
| 13 |
+
|
| 14 |
+
console.log("\n--- REFORZANDO REGLA 0 (Curiosidad) ---");
|
| 15 |
+
// Reforzamos la regla 0 tres veces para ver el impacto acumulado
|
| 16 |
+
engine.reinforceRule(0, 0.2);
|
| 17 |
+
engine.reinforceRule(0, 0.2);
|
| 18 |
+
engine.reinforceRule(0, 0.2);
|
| 19 |
+
|
| 20 |
+
console.log("\n--- DESPUÉS DEL REFUERZO ---");
|
| 21 |
+
inf = engine.infer(state, context);
|
| 22 |
+
console.log("Reglas activas:", inf.activeRules);
|
| 23 |
+
console.log("Confianza:", inf.inferenceConfidence.toFixed(4));
|
| 24 |
+
|
| 25 |
+
if (inf.activeRules.includes(0)) {
|
| 26 |
+
console.log("\nResultado: El sistema ahora confía más en la Regla #0.");
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
testReinforce().catch(console.error);
|
src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts
CHANGED
|
@@ -341,6 +341,173 @@ describe("runReplyAgent heartbeat followup guard", () => {
|
|
| 341 |
});
|
| 342 |
});
|
| 343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
describe("runReplyAgent typing (heartbeat)", () => {
|
| 345 |
async function withTempStateDir<T>(fn: (stateDir: string) => Promise<T>): Promise<T> {
|
| 346 |
return await withStateDirEnv(
|
|
|
|
| 341 |
});
|
| 342 |
});
|
| 343 |
|
| 344 |
+
describe("runReplyAgent interrupted auto-resume backoff", () => {
|
| 345 |
+
it("backs off rate-limited auto-resume retries and allows a fourth attempt", async () => {
|
| 346 |
+
await withStateDirEnv("openclaw-rate-limit-auto-resume-", async ({ stateDir }) => {
|
| 347 |
+
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
| 348 |
+
const now = Date.now();
|
| 349 |
+
const sessionEntry: SessionEntry = {
|
| 350 |
+
sessionId: "session",
|
| 351 |
+
updatedAt: now,
|
| 352 |
+
interruptedTurn: {
|
| 353 |
+
runId: "prior-turn",
|
| 354 |
+
message: "continue the prior task",
|
| 355 |
+
startedAt: now - 5_000,
|
| 356 |
+
resumeCount: 3,
|
| 357 |
+
lastResumeAt: now - 1_000,
|
| 358 |
+
},
|
| 359 |
+
};
|
| 360 |
+
const sessionStore = { main: sessionEntry };
|
| 361 |
+
await seedSessionStore({
|
| 362 |
+
storePath,
|
| 363 |
+
sessionKey: "main",
|
| 364 |
+
entry: sessionEntry,
|
| 365 |
+
});
|
| 366 |
+
|
| 367 |
+
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
| 368 |
+
payloads: [],
|
| 369 |
+
meta: {
|
| 370 |
+
stopReason: "error",
|
| 371 |
+
error: {
|
| 372 |
+
message:
|
| 373 |
+
"Cloud Code Assist API error (429): No capacity available for model gemini-3-flash-preview on the server",
|
| 374 |
+
},
|
| 375 |
+
},
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
const fallbackSpy = vi
|
| 379 |
+
.spyOn(modelFallbackModule, "runWithModelFallback")
|
| 380 |
+
.mockImplementationOnce(
|
| 381 |
+
async ({
|
| 382 |
+
provider,
|
| 383 |
+
model,
|
| 384 |
+
run,
|
| 385 |
+
}: {
|
| 386 |
+
provider: string;
|
| 387 |
+
model: string;
|
| 388 |
+
run: (provider: string, model: string) => Promise<unknown>;
|
| 389 |
+
}) => ({
|
| 390 |
+
result: await run(provider, model),
|
| 391 |
+
provider,
|
| 392 |
+
model,
|
| 393 |
+
attempts: [
|
| 394 |
+
{
|
| 395 |
+
provider,
|
| 396 |
+
model,
|
| 397 |
+
error: "429 No capacity available",
|
| 398 |
+
reason: "rate_limit",
|
| 399 |
+
},
|
| 400 |
+
],
|
| 401 |
+
}),
|
| 402 |
+
);
|
| 403 |
+
|
| 404 |
+
try {
|
| 405 |
+
vi.mocked(enqueueFollowupRun).mockReturnValueOnce(true);
|
| 406 |
+
const { run } = createMinimalRun({
|
| 407 |
+
sessionEntry,
|
| 408 |
+
sessionStore,
|
| 409 |
+
sessionKey: "main",
|
| 410 |
+
storePath,
|
| 411 |
+
});
|
| 412 |
+
const result = await run();
|
| 413 |
+
|
| 414 |
+
expect(result).toMatchObject({
|
| 415 |
+
text: expect.stringContaining("queued an automatic retry"),
|
| 416 |
+
});
|
| 417 |
+
expect(vi.mocked(enqueueFollowupRun)).toHaveBeenCalledTimes(1);
|
| 418 |
+
const [_queueKey, queuedRun, queueSettings] = vi.mocked(enqueueFollowupRun).mock.calls[0]!;
|
| 419 |
+
expect((queuedRun as FollowupRun).messageId).toContain(":4");
|
| 420 |
+
expect(queueSettings).toMatchObject({ debounceMs: 10_000 });
|
| 421 |
+
expect(sessionStore.main.interruptedTurn?.resumeCount).toBe(4);
|
| 422 |
+
|
| 423 |
+
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
|
| 424 |
+
expect(persisted.main.interruptedTurn.resumeCount).toBe(4);
|
| 425 |
+
} finally {
|
| 426 |
+
fallbackSpy.mockRestore();
|
| 427 |
+
}
|
| 428 |
+
});
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
+
it("does not enqueue a fifth rate-limited auto-resume retry", async () => {
|
| 432 |
+
await withStateDirEnv("openclaw-rate-limit-auto-resume-limit-", async ({ stateDir }) => {
|
| 433 |
+
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
| 434 |
+
const now = Date.now();
|
| 435 |
+
const sessionEntry: SessionEntry = {
|
| 436 |
+
sessionId: "session",
|
| 437 |
+
updatedAt: now,
|
| 438 |
+
interruptedTurn: {
|
| 439 |
+
runId: "prior-turn",
|
| 440 |
+
message: "continue the prior task",
|
| 441 |
+
startedAt: now - 5_000,
|
| 442 |
+
resumeCount: 4,
|
| 443 |
+
lastResumeAt: now - 1_000,
|
| 444 |
+
},
|
| 445 |
+
};
|
| 446 |
+
const sessionStore = { main: sessionEntry };
|
| 447 |
+
await seedSessionStore({
|
| 448 |
+
storePath,
|
| 449 |
+
sessionKey: "main",
|
| 450 |
+
entry: sessionEntry,
|
| 451 |
+
});
|
| 452 |
+
|
| 453 |
+
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
| 454 |
+
payloads: [],
|
| 455 |
+
meta: {
|
| 456 |
+
stopReason: "error",
|
| 457 |
+
error: {
|
| 458 |
+
message:
|
| 459 |
+
"Cloud Code Assist API error (429): No capacity available for model gemini-3-flash-preview on the server",
|
| 460 |
+
},
|
| 461 |
+
},
|
| 462 |
+
});
|
| 463 |
+
|
| 464 |
+
const fallbackSpy = vi
|
| 465 |
+
.spyOn(modelFallbackModule, "runWithModelFallback")
|
| 466 |
+
.mockImplementationOnce(
|
| 467 |
+
async ({
|
| 468 |
+
provider,
|
| 469 |
+
model,
|
| 470 |
+
run,
|
| 471 |
+
}: {
|
| 472 |
+
provider: string;
|
| 473 |
+
model: string;
|
| 474 |
+
run: (provider: string, model: string) => Promise<unknown>;
|
| 475 |
+
}) => ({
|
| 476 |
+
result: await run(provider, model),
|
| 477 |
+
provider,
|
| 478 |
+
model,
|
| 479 |
+
attempts: [
|
| 480 |
+
{
|
| 481 |
+
provider,
|
| 482 |
+
model,
|
| 483 |
+
error: "429 No capacity available",
|
| 484 |
+
reason: "rate_limit",
|
| 485 |
+
},
|
| 486 |
+
],
|
| 487 |
+
}),
|
| 488 |
+
);
|
| 489 |
+
|
| 490 |
+
try {
|
| 491 |
+
const { run } = createMinimalRun({
|
| 492 |
+
sessionEntry,
|
| 493 |
+
sessionStore,
|
| 494 |
+
sessionKey: "main",
|
| 495 |
+
storePath,
|
| 496 |
+
});
|
| 497 |
+
const result = await run();
|
| 498 |
+
|
| 499 |
+
expect(result).toMatchObject({
|
| 500 |
+
text: expect.stringContaining("Please retry"),
|
| 501 |
+
});
|
| 502 |
+
expect(vi.mocked(enqueueFollowupRun)).not.toHaveBeenCalled();
|
| 503 |
+
expect(sessionStore.main.interruptedTurn?.resumeCount).toBe(4);
|
| 504 |
+
} finally {
|
| 505 |
+
fallbackSpy.mockRestore();
|
| 506 |
+
}
|
| 507 |
+
});
|
| 508 |
+
});
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
describe("runReplyAgent typing (heartbeat)", () => {
|
| 512 |
async function withTempStateDir<T>(fn: (stateDir: string) => Promise<T>): Promise<T> {
|
| 513 |
return await withStateDirEnv(
|
src/auto-reply/reply/agent-runner.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { lookupContextTokens } from "../../agents/context.js";
|
|
| 3 |
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
| 4 |
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
| 5 |
import { isCliProvider } from "../../agents/model-selection.js";
|
|
|
|
| 6 |
import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
|
| 7 |
import { hasNonzeroUsage } from "../../agents/usage.js";
|
| 8 |
import {
|
| 9 |
canAttemptInterruptedResume,
|
|
|
|
| 10 |
markSessionUnfinishedTurn,
|
| 11 |
markSessionInterruptedResumeAttempt,
|
| 12 |
resolveAgentIdFromSessionKey,
|
|
@@ -33,6 +35,7 @@ import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
|
| 33 |
import { resolveResponseUsageMode, type VerboseLevel } from "../thinking.js";
|
| 34 |
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
| 35 |
import { runAgentTurnWithFallback } from "./agent-runner-execution.js";
|
|
|
|
| 36 |
import {
|
| 37 |
createShouldEmitToolOutput,
|
| 38 |
createShouldEmitToolResult,
|
|
@@ -69,6 +72,33 @@ import { createTypingSignaler } from "./typing-mode.js";
|
|
| 69 |
import type { TypingController } from "./typing.js";
|
| 70 |
|
| 71 |
const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
export async function runReplyAgent(params: {
|
| 74 |
commandBody: string;
|
|
@@ -285,12 +315,15 @@ export async function runReplyAgent(params: {
|
|
| 285 |
agentCfgContextTokens,
|
| 286 |
});
|
| 287 |
|
| 288 |
-
const maybeQueueInterruptedRetry = async () => {
|
| 289 |
if (!sessionKey || !storePath || !currentUnfinishedTurn) {
|
| 290 |
return false;
|
| 291 |
}
|
| 292 |
const entryForResume = activeSessionStore?.[sessionKey] ?? activeSessionEntry;
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
| 294 |
return false;
|
| 295 |
}
|
| 296 |
const updatedEntry = await markSessionInterruptedResumeAttempt({
|
|
@@ -299,6 +332,14 @@ export async function runReplyAgent(params: {
|
|
| 299 |
storePath,
|
| 300 |
});
|
| 301 |
const nextResumeCount = updatedEntry?.interruptedTurn?.resumeCount ?? 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
const enqueued = enqueueFollowupRun(
|
| 303 |
queueKey,
|
| 304 |
{
|
|
@@ -309,7 +350,7 @@ export async function runReplyAgent(params: {
|
|
| 309 |
? `Auto-resume: ${followupRun.summaryLine}`
|
| 310 |
: "Auto-resume interrupted turn",
|
| 311 |
},
|
| 312 |
-
|
| 313 |
);
|
| 314 |
if (enqueued && updatedEntry) {
|
| 315 |
activeSessionEntry = updatedEntry;
|
|
@@ -562,6 +603,10 @@ export async function runReplyAgent(params: {
|
|
| 562 |
hadExecutionError: runResult.meta?.stopReason === "error" || Boolean(runResult.meta?.error),
|
| 563 |
hasActiveBackgroundTask,
|
| 564 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
// Drain any late tool/block deliveries before deciding there's "nothing to send".
|
| 567 |
// Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
|
|
@@ -569,7 +614,9 @@ export async function runReplyAgent(params: {
|
|
| 569 |
if (payloadArray.length === 0) {
|
| 570 |
const turnOutcome = resolveTurnOutcome(0);
|
| 571 |
if (turnOutcome.shouldSynthesizeDegradedReply) {
|
| 572 |
-
const autoResumeQueued = await maybeQueueInterruptedRetry(
|
|
|
|
|
|
|
| 573 |
return finalizeWithFollowup(
|
| 574 |
hasActiveBackgroundTask
|
| 575 |
? autoResumeQueued
|
|
@@ -613,7 +660,9 @@ export async function runReplyAgent(params: {
|
|
| 613 |
if (replyPayloads.length === 0) {
|
| 614 |
const turnOutcome = resolveTurnOutcome(0);
|
| 615 |
if (turnOutcome.shouldSynthesizeDegradedReply) {
|
| 616 |
-
const autoResumeQueued = await maybeQueueInterruptedRetry(
|
|
|
|
|
|
|
| 617 |
return finalizeWithFollowup(
|
| 618 |
hasActiveBackgroundTask
|
| 619 |
? autoResumeQueued
|
|
|
|
| 3 |
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
| 4 |
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
| 5 |
import { isCliProvider } from "../../agents/model-selection.js";
|
| 6 |
+
import { classifyFailoverReason } from "../../agents/pi-embedded-helpers.js";
|
| 7 |
import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
|
| 8 |
import { hasNonzeroUsage } from "../../agents/usage.js";
|
| 9 |
import {
|
| 10 |
canAttemptInterruptedResume,
|
| 11 |
+
MAX_RATE_LIMIT_INTERRUPTED_RESUME_COUNT,
|
| 12 |
markSessionUnfinishedTurn,
|
| 13 |
markSessionInterruptedResumeAttempt,
|
| 14 |
resolveAgentIdFromSessionKey,
|
|
|
|
| 35 |
import { resolveResponseUsageMode, type VerboseLevel } from "../thinking.js";
|
| 36 |
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
| 37 |
import { runAgentTurnWithFallback } from "./agent-runner-execution.js";
|
| 38 |
+
import type { RuntimeFallbackAttempt } from "./agent-runner-execution.js";
|
| 39 |
import {
|
| 40 |
createShouldEmitToolOutput,
|
| 41 |
createShouldEmitToolResult,
|
|
|
|
| 72 |
import type { TypingController } from "./typing.js";
|
| 73 |
|
| 74 |
const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
|
| 75 |
+
const RATE_LIMIT_INTERRUPTED_RESUME_DELAY_MS = 10_000;
|
| 76 |
+
|
| 77 |
+
function extractRunErrorMessage(error: unknown): string | undefined {
|
| 78 |
+
if (typeof error === "string") {
|
| 79 |
+
const trimmed = error.trim();
|
| 80 |
+
return trimmed || undefined;
|
| 81 |
+
}
|
| 82 |
+
if (!error || typeof error !== "object") {
|
| 83 |
+
return undefined;
|
| 84 |
+
}
|
| 85 |
+
if ("message" in error && typeof error.message === "string") {
|
| 86 |
+
const trimmed = error.message.trim();
|
| 87 |
+
return trimmed || undefined;
|
| 88 |
+
}
|
| 89 |
+
return undefined;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function shouldBackoffInterruptedRetry(params: {
|
| 93 |
+
fallbackAttempts: RuntimeFallbackAttempt[];
|
| 94 |
+
runError?: unknown;
|
| 95 |
+
}): boolean {
|
| 96 |
+
if (params.fallbackAttempts.some((attempt) => attempt.reason === "rate_limit")) {
|
| 97 |
+
return true;
|
| 98 |
+
}
|
| 99 |
+
const rawError = extractRunErrorMessage(params.runError);
|
| 100 |
+
return rawError ? classifyFailoverReason(rawError) === "rate_limit" : false;
|
| 101 |
+
}
|
| 102 |
|
| 103 |
export async function runReplyAgent(params: {
|
| 104 |
commandBody: string;
|
|
|
|
| 315 |
agentCfgContextTokens,
|
| 316 |
});
|
| 317 |
|
| 318 |
+
const maybeQueueInterruptedRetry = async (options?: { rateLimitBackoff?: boolean }) => {
|
| 319 |
if (!sessionKey || !storePath || !currentUnfinishedTurn) {
|
| 320 |
return false;
|
| 321 |
}
|
| 322 |
const entryForResume = activeSessionStore?.[sessionKey] ?? activeSessionEntry;
|
| 323 |
+
const maxResumeCount = options?.rateLimitBackoff
|
| 324 |
+
? MAX_RATE_LIMIT_INTERRUPTED_RESUME_COUNT
|
| 325 |
+
: undefined;
|
| 326 |
+
if (!canAttemptInterruptedResume(entryForResume, { maxResumeCount })) {
|
| 327 |
return false;
|
| 328 |
}
|
| 329 |
const updatedEntry = await markSessionInterruptedResumeAttempt({
|
|
|
|
| 332 |
storePath,
|
| 333 |
});
|
| 334 |
const nextResumeCount = updatedEntry?.interruptedTurn?.resumeCount ?? 1;
|
| 335 |
+
const queueSettings =
|
| 336 |
+
options?.rateLimitBackoff &&
|
| 337 |
+
(resolvedQueue.debounceMs ?? 0) < RATE_LIMIT_INTERRUPTED_RESUME_DELAY_MS
|
| 338 |
+
? {
|
| 339 |
+
...resolvedQueue,
|
| 340 |
+
debounceMs: RATE_LIMIT_INTERRUPTED_RESUME_DELAY_MS,
|
| 341 |
+
}
|
| 342 |
+
: resolvedQueue;
|
| 343 |
const enqueued = enqueueFollowupRun(
|
| 344 |
queueKey,
|
| 345 |
{
|
|
|
|
| 350 |
? `Auto-resume: ${followupRun.summaryLine}`
|
| 351 |
: "Auto-resume interrupted turn",
|
| 352 |
},
|
| 353 |
+
queueSettings,
|
| 354 |
);
|
| 355 |
if (enqueued && updatedEntry) {
|
| 356 |
activeSessionEntry = updatedEntry;
|
|
|
|
| 603 |
hadExecutionError: runResult.meta?.stopReason === "error" || Boolean(runResult.meta?.error),
|
| 604 |
hasActiveBackgroundTask,
|
| 605 |
});
|
| 606 |
+
const shouldBackoffAutoResume = shouldBackoffInterruptedRetry({
|
| 607 |
+
fallbackAttempts,
|
| 608 |
+
runError: runResult.meta?.error,
|
| 609 |
+
});
|
| 610 |
|
| 611 |
// Drain any late tool/block deliveries before deciding there's "nothing to send".
|
| 612 |
// Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
|
|
|
|
| 614 |
if (payloadArray.length === 0) {
|
| 615 |
const turnOutcome = resolveTurnOutcome(0);
|
| 616 |
if (turnOutcome.shouldSynthesizeDegradedReply) {
|
| 617 |
+
const autoResumeQueued = await maybeQueueInterruptedRetry({
|
| 618 |
+
rateLimitBackoff: shouldBackoffAutoResume,
|
| 619 |
+
});
|
| 620 |
return finalizeWithFollowup(
|
| 621 |
hasActiveBackgroundTask
|
| 622 |
? autoResumeQueued
|
|
|
|
| 660 |
if (replyPayloads.length === 0) {
|
| 661 |
const turnOutcome = resolveTurnOutcome(0);
|
| 662 |
if (turnOutcome.shouldSynthesizeDegradedReply) {
|
| 663 |
+
const autoResumeQueued = await maybeQueueInterruptedRetry({
|
| 664 |
+
rateLimitBackoff: shouldBackoffAutoResume,
|
| 665 |
+
});
|
| 666 |
return finalizeWithFollowup(
|
| 667 |
hasActiveBackgroundTask
|
| 668 |
? autoResumeQueued
|
src/config/sessions/unfinished-turn.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { SessionEntry, SessionInterruptedTurn, SessionUnfinishedTurn } from
|
|
| 5 |
|
| 6 |
const UNFINISHED_TURN_PREVIEW_MAX_CHARS = 240;
|
| 7 |
export const MAX_INTERRUPTED_RESUME_COUNT = 3;
|
|
|
|
| 8 |
|
| 9 |
function normalizeUnfinishedTurnPreview(text: string): string | undefined {
|
| 10 |
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
@@ -94,9 +95,18 @@ export async function markSessionUnfinishedTurn(params: {
|
|
| 94 |
return unfinishedTurn;
|
| 95 |
}
|
| 96 |
|
| 97 |
-
export function canAttemptInterruptedResume(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
const resumeCount = entry?.interruptedTurn?.resumeCount ?? 0;
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
export async function markSessionInterruptedResumeAttempt(params: {
|
|
|
|
| 5 |
|
| 6 |
const UNFINISHED_TURN_PREVIEW_MAX_CHARS = 240;
|
| 7 |
export const MAX_INTERRUPTED_RESUME_COUNT = 3;
|
| 8 |
+
export const MAX_RATE_LIMIT_INTERRUPTED_RESUME_COUNT = 4;
|
| 9 |
|
| 10 |
function normalizeUnfinishedTurnPreview(text: string): string | undefined {
|
| 11 |
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
|
|
| 95 |
return unfinishedTurn;
|
| 96 |
}
|
| 97 |
|
| 98 |
+
export function canAttemptInterruptedResume(
|
| 99 |
+
entry: SessionEntry | undefined,
|
| 100 |
+
options?: {
|
| 101 |
+
maxResumeCount?: number;
|
| 102 |
+
},
|
| 103 |
+
): boolean {
|
| 104 |
const resumeCount = entry?.interruptedTurn?.resumeCount ?? 0;
|
| 105 |
+
const maxResumeCount =
|
| 106 |
+
typeof options?.maxResumeCount === "number" && Number.isFinite(options.maxResumeCount)
|
| 107 |
+
? Math.max(1, Math.floor(options.maxResumeCount))
|
| 108 |
+
: MAX_INTERRUPTED_RESUME_COUNT;
|
| 109 |
+
return resumeCount < maxResumeCount;
|
| 110 |
}
|
| 111 |
|
| 112 |
export async function markSessionInterruptedResumeAttempt(params: {
|
src/infra/runtime-failure.test.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from "vitest";
|
| 2 |
+
import { classifyOpenSkynetRuntimeFailure } from "./runtime-failure.js";
|
| 3 |
+
|
| 4 |
+
describe("classifyOpenSkynetRuntimeFailure", () => {
|
| 5 |
+
it("classifies provider rate limits as environmental", () => {
|
| 6 |
+
expect(
|
| 7 |
+
classifyOpenSkynetRuntimeFailure({
|
| 8 |
+
status: "error",
|
| 9 |
+
errorText: "429 No capacity available for model gemini",
|
| 10 |
+
}),
|
| 11 |
+
).toEqual({
|
| 12 |
+
failureDomain: "environmental",
|
| 13 |
+
failureClass: "provider_rate_limit",
|
| 14 |
+
});
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
it("classifies session locks as environmental", () => {
|
| 18 |
+
expect(
|
| 19 |
+
classifyOpenSkynetRuntimeFailure({
|
| 20 |
+
status: "error",
|
| 21 |
+
errorText: "session file locked (timeout 30000ms): owner /tmp/session.lock",
|
| 22 |
+
}),
|
| 23 |
+
).toEqual({
|
| 24 |
+
failureDomain: "environmental",
|
| 25 |
+
failureClass: "session_lock",
|
| 26 |
+
});
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
it("classifies missing paths as cognitive", () => {
|
| 30 |
+
expect(
|
| 31 |
+
classifyOpenSkynetRuntimeFailure({
|
| 32 |
+
status: "error",
|
| 33 |
+
errorText: "ENOENT: no such file or directory",
|
| 34 |
+
}),
|
| 35 |
+
).toEqual({
|
| 36 |
+
failureDomain: "cognitive",
|
| 37 |
+
failureClass: "missing_path",
|
| 38 |
+
});
|
| 39 |
+
});
|
| 40 |
+
});
|
src/infra/runtime-failure.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type OpenSkynetRuntimeFailureDomain = "none" | "environmental" | "cognitive" | "mixed";
|
| 2 |
+
|
| 3 |
+
export type OpenSkynetRuntimeFailureClass =
|
| 4 |
+
| "none"
|
| 5 |
+
| "provider_rate_limit"
|
| 6 |
+
| "provider_timeout"
|
| 7 |
+
| "gateway_restart"
|
| 8 |
+
| "gateway_connection"
|
| 9 |
+
| "permission_denied"
|
| 10 |
+
| "session_lock"
|
| 11 |
+
| "missing_path"
|
| 12 |
+
| "validation_error"
|
| 13 |
+
| "unknown_error";
|
| 14 |
+
|
| 15 |
+
export type OpenSkynetRuntimeFailure = {
|
| 16 |
+
failureDomain: OpenSkynetRuntimeFailureDomain;
|
| 17 |
+
failureClass: OpenSkynetRuntimeFailureClass;
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
function normalizeFailureText(input: unknown): string {
|
| 21 |
+
return typeof input === "string" ? input.toLowerCase() : "";
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function classifyOpenSkynetRuntimeFailure(params: {
|
| 25 |
+
status?: string;
|
| 26 |
+
errorText?: string;
|
| 27 |
+
isOk?: boolean;
|
| 28 |
+
}): OpenSkynetRuntimeFailure {
|
| 29 |
+
const status = normalizeFailureText(params.status);
|
| 30 |
+
const errorText = normalizeFailureText(params.errorText);
|
| 31 |
+
const isOk = params.isOk === true;
|
| 32 |
+
|
| 33 |
+
if (isOk) {
|
| 34 |
+
return { failureDomain: "none", failureClass: "none" };
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (
|
| 38 |
+
errorText.includes("rate limit") ||
|
| 39 |
+
errorText.includes("no capacity available") ||
|
| 40 |
+
errorText.includes("resource exhausted") ||
|
| 41 |
+
errorText.includes("api rate limit reached") ||
|
| 42 |
+
errorText.includes("too many requests") ||
|
| 43 |
+
errorText.includes("429")
|
| 44 |
+
) {
|
| 45 |
+
return { failureDomain: "environmental", failureClass: "provider_rate_limit" };
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if (
|
| 49 |
+
errorText.includes("service restart") ||
|
| 50 |
+
errorText.includes("config change detected") ||
|
| 51 |
+
errorText.includes("restarting") ||
|
| 52 |
+
errorText.includes("wait for active embedded runs timed out")
|
| 53 |
+
) {
|
| 54 |
+
return { failureDomain: "environmental", failureClass: "gateway_restart" };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if (errorText.includes("session file locked") || errorText.includes("lock timeout")) {
|
| 58 |
+
return { failureDomain: "environmental", failureClass: "session_lock" };
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if (status === "timeout" || errorText.includes("timed out") || errorText.includes("timeout")) {
|
| 62 |
+
return { failureDomain: "environmental", failureClass: "provider_timeout" };
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if (
|
| 66 |
+
errorText.includes("gateway closed") ||
|
| 67 |
+
errorText.includes("connection reset") ||
|
| 68 |
+
errorText.includes("connection refused") ||
|
| 69 |
+
errorText.includes("token mismatch")
|
| 70 |
+
) {
|
| 71 |
+
return { failureDomain: "environmental", failureClass: "gateway_connection" };
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if (
|
| 75 |
+
errorText.includes("permission denied") ||
|
| 76 |
+
errorText.includes("eacces") ||
|
| 77 |
+
errorText.includes("operation not permitted")
|
| 78 |
+
) {
|
| 79 |
+
return { failureDomain: "environmental", failureClass: "permission_denied" };
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (
|
| 83 |
+
errorText.includes("enoent") ||
|
| 84 |
+
errorText.includes("no such file") ||
|
| 85 |
+
errorText.includes("cannot find")
|
| 86 |
+
) {
|
| 87 |
+
return { failureDomain: "cognitive", failureClass: "missing_path" };
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if (
|
| 91 |
+
errorText.includes("syntax error") ||
|
| 92 |
+
errorText.includes("type error") ||
|
| 93 |
+
errorText.includes("validation failed") ||
|
| 94 |
+
errorText.includes("test failed")
|
| 95 |
+
) {
|
| 96 |
+
return { failureDomain: "cognitive", failureClass: "validation_error" };
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return { failureDomain: "mixed", failureClass: "unknown_error" };
|
| 100 |
+
}
|
src/skynet/experiments/EX/SKYNET_CORE_V80_HYPERGRAPH.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SKYNET CORE V80: HYPERGRAPH (The Liquid Brain)
|
| 3 |
+
==============================================
|
| 4 |
+
|
| 5 |
+
This core represents the integration of Biphasic Physics and Dynamic Topology.
|
| 6 |
+
It moves beyond fixed matrices to a fully autopoietic substrate where
|
| 7 |
+
matter (nodes) creates space (adjacency) on the fly based on the signal.
|
| 8 |
+
|
| 9 |
+
Key Innovations:
|
| 10 |
+
1. Dynamic Topology: The adjacency matrix A_t evolves via Hebbian Plasticity.
|
| 11 |
+
2. Topological Wells: The Mexican Hat force allows nodes to "crystallize"
|
| 12 |
+
and resist catastrophic erasure and noise.
|
| 13 |
+
3. Liquid Diffusion: Information flows through the dynamic network, allowing
|
| 14 |
+
logic-over-time and spatial abstraction.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
|
| 21 |
+
class LiquidBrainOrgan(nn.Module):
|
| 22 |
+
"""
|
| 23 |
+
The physical organ of the V80 Hypergraph.
|
| 24 |
+
"""
|
| 25 |
+
def __init__(self, n_nodes=32, d_feature=8):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.n_nodes = n_nodes
|
| 28 |
+
self.d_feature = d_feature
|
| 29 |
+
self.hidden_dim = n_nodes * d_feature
|
| 30 |
+
|
| 31 |
+
# Physics Parameters
|
| 32 |
+
self.mu = nn.Parameter(torch.tensor(0.45))
|
| 33 |
+
self.sigma = nn.Parameter(torch.tensor(0.35))
|
| 34 |
+
|
| 35 |
+
# Plasticity
|
| 36 |
+
self.plasticity_rate = nn.Parameter(torch.tensor(0.05))
|
| 37 |
+
self.decay_rate = nn.Parameter(torch.tensor(0.005))
|
| 38 |
+
|
| 39 |
+
def g_fluid(self, h):
|
| 40 |
+
return 2.0 * torch.exp(-((h - self.mu)**2) / (2 * self.sigma**2 + 1e-6)) - 1.0
|
| 41 |
+
|
| 42 |
+
def forward(self, h_ctx, x_in, A_prev=None, h_prev=None):
|
| 43 |
+
"""
|
| 44 |
+
Single step forward for the physical organ.
|
| 45 |
+
x_in: [B, N, F] drive from cortex/input
|
| 46 |
+
h_prev: [B, N, F] previous physical state
|
| 47 |
+
A_prev: [B, N, N] previous topology
|
| 48 |
+
"""
|
| 49 |
+
batch = x_in.shape[0]
|
| 50 |
+
|
| 51 |
+
if h_prev is None:
|
| 52 |
+
h_prev = torch.zeros(batch, self.n_nodes, self.d_feature, device=x_in.device)
|
| 53 |
+
if A_prev is None:
|
| 54 |
+
A_prev = torch.eye(self.n_nodes, device=x_in.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 55 |
+
|
| 56 |
+
# 1. Input injection with damping
|
| 57 |
+
x_damped = x_in / (1.0 + torch.norm(x_in, dim=-1, keepdim=True) * 0.1)
|
| 58 |
+
|
| 59 |
+
# 2. Biphasic Interaction + Topological Well (Mexican Hat)
|
| 60 |
+
h_core = torch.tanh(h_prev + 0.5 * x_damped)
|
| 61 |
+
force = (h_core - torch.pow(h_core, 3)).detach()
|
| 62 |
+
h = h_core + 0.35 * force
|
| 63 |
+
|
| 64 |
+
# 3. Liquid Diffusion
|
| 65 |
+
A_norm = A_prev / (A_prev.sum(dim=-1, keepdim=True) + 1e-6)
|
| 66 |
+
h_diffused = torch.bmm(A_norm, h)
|
| 67 |
+
h = h + 0.2 * (h_diffused - h)
|
| 68 |
+
|
| 69 |
+
# 4. Topology Update (Plasticity)
|
| 70 |
+
h_normed = F.normalize(h, dim=-1)
|
| 71 |
+
corr = torch.bmm(h_normed, h_normed.transpose(1, 2))
|
| 72 |
+
|
| 73 |
+
eta = torch.sigmoid(self.plasticity_rate) * 0.05
|
| 74 |
+
lam = torch.sigmoid(self.decay_rate) * 0.01
|
| 75 |
+
|
| 76 |
+
A_next = torch.clamp(A_prev + eta * corr - lam * A_prev, 0.0, 1.0)
|
| 77 |
+
|
| 78 |
+
# Self-loop preservation
|
| 79 |
+
idx = torch.arange(self.n_nodes, device=x_in.device)
|
| 80 |
+
A_next[:, idx, idx] = 1.0
|
| 81 |
+
|
| 82 |
+
h_next = torch.tanh(h)
|
| 83 |
+
|
| 84 |
+
return h_next, A_next
|
| 85 |
+
|
| 86 |
+
class SKYNET_CORE_V80_HYPERGRAPH(nn.Module):
|
| 87 |
+
"""
|
| 88 |
+
SKYNET V80: THE HYPERGRAPH
|
| 89 |
+
Neural Cortex coupled with the Liquid Brain Organ.
|
| 90 |
+
"""
|
| 91 |
+
def __init__(self, n_input=658, n_actions=20, d_model=128, n_nodes=32, d_feature=8, device='cuda'):
|
| 92 |
+
super().__init__()
|
| 93 |
+
self.device = device
|
| 94 |
+
self.d_model = d_model
|
| 95 |
+
self.n_nodes = n_nodes
|
| 96 |
+
self.d_feature = d_feature
|
| 97 |
+
|
| 98 |
+
self.input_proj = nn.Linear(n_input, d_model)
|
| 99 |
+
self.input_norm = nn.LayerNorm(d_model)
|
| 100 |
+
|
| 101 |
+
self.cortex = nn.GRU(d_model, d_model, batch_first=True)
|
| 102 |
+
|
| 103 |
+
self.phys_proj = nn.Linear(d_model, n_nodes * d_feature)
|
| 104 |
+
self.organ = LiquidBrainOrgan(n_nodes=n_nodes, d_feature=d_feature)
|
| 105 |
+
|
| 106 |
+
# Readout
|
| 107 |
+
self.readout = nn.Linear(d_model + (n_nodes * d_feature), n_actions)
|
| 108 |
+
|
| 109 |
+
self.cortex_state = None
|
| 110 |
+
self.organ_state = None
|
| 111 |
+
self.topology_state = None
|
| 112 |
+
|
| 113 |
+
def reset(self):
|
| 114 |
+
self.cortex_state = None
|
| 115 |
+
self.organ_state = None
|
| 116 |
+
self.topology_state = None
|
| 117 |
+
|
| 118 |
+
def detach_states(self):
|
| 119 |
+
if self.cortex_state is not None: self.cortex_state = self.cortex_state.detach()
|
| 120 |
+
if self.organ_state is not None: self.organ_state = self.organ_state.detach()
|
| 121 |
+
if self.topology_state is not None: self.topology_state = self.topology_state.detach()
|
| 122 |
+
|
| 123 |
+
def forward_sequence(self, x_seq, training=True):
|
| 124 |
+
"""
|
| 125 |
+
Processes a full sequence and returns the final logits.
|
| 126 |
+
Compatible with sequential benchmarks.
|
| 127 |
+
"""
|
| 128 |
+
self.reset()
|
| 129 |
+
batch, steps, _ = x_seq.shape
|
| 130 |
+
for t in range(steps):
|
| 131 |
+
out = self.forward(x_seq[:, t], training=training)
|
| 132 |
+
return out['logits']
|
| 133 |
+
|
| 134 |
+
def forward(self, x, training=True):
|
| 135 |
+
batch = x.shape[0]
|
| 136 |
+
if x.dim() == 3:
|
| 137 |
+
x = x.view(batch, -1)
|
| 138 |
+
|
| 139 |
+
h_in = self.input_norm(self.input_proj(x))
|
| 140 |
+
|
| 141 |
+
if self.cortex_state is None or self.cortex_state.shape[1] != batch:
|
| 142 |
+
self.cortex_state = torch.zeros(1, batch, self.d_model, device=x.device)
|
| 143 |
+
|
| 144 |
+
h_ctx, self.cortex_state = self.cortex(h_in.unsqueeze(1), self.cortex_state)
|
| 145 |
+
h_ctx = h_ctx.squeeze(1)
|
| 146 |
+
|
| 147 |
+
# Drive the physical organ
|
| 148 |
+
phys_drive = self.phys_proj(h_ctx).view(batch, self.n_nodes, self.d_feature)
|
| 149 |
+
|
| 150 |
+
self.organ_state, self.topology_state = self.organ(
|
| 151 |
+
h_ctx, phys_drive, A_prev=self.topology_state, h_prev=self.organ_state
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
h_phys_flat = self.organ_state.view(batch, -1)
|
| 155 |
+
h_fused = torch.cat([h_ctx, h_phys_flat], dim=-1)
|
| 156 |
+
|
| 157 |
+
logits = self.readout(h_fused)
|
| 158 |
+
probs = F.softmax(logits, dim=-1)
|
| 159 |
+
entropy = -(probs * torch.log(probs + 1e-6)).sum(-1, keepdim=True)
|
| 160 |
+
|
| 161 |
+
audit = {
|
| 162 |
+
'topology_density': self.topology_state.mean().item(),
|
| 163 |
+
'phys_flux': self.organ_state.abs().mean().item(),
|
| 164 |
+
'entropy': entropy.mean().item()
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
'logits': logits,
|
| 169 |
+
'probs': probs,
|
| 170 |
+
'entropy': entropy,
|
| 171 |
+
'audit': audit
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 176 |
+
model = SKYNET_CORE_V80_HYPERGRAPH(device=device).to(device)
|
| 177 |
+
x = torch.randn(4, 658, device=device)
|
| 178 |
+
out = model(x)
|
| 179 |
+
print("V80 Hypergraph Forward Pass Successful.")
|
| 180 |
+
print(f"Logits Shape: {out['logits'].shape}")
|
| 181 |
+
print(f"Audit: {out['audit']}")
|
src/skynet/experiments/EX/SKYNET_CORE_V85_SCALING_HYPERGRAPH.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SKYNET CORE V85: SCALING HYPERGRAPH (The Living Brain)
|
| 3 |
+
=====================================================
|
| 4 |
+
|
| 5 |
+
Integration of V80 (Dynamic Topology) with V85 (Neurogenesis & Pruning).
|
| 6 |
+
This core implements physical brain expansion and synaptic cleaning.
|
| 7 |
+
|
| 8 |
+
Features:
|
| 9 |
+
1. Dynamic Topology: Adjacency A_t evolves via Hebbian Plasticity.
|
| 10 |
+
2. Neurogenesis: Adds nodes when hidden state saturation is detected.
|
| 11 |
+
3. Synaptic Pruning: Deletes edges with weight < threshold.
|
| 12 |
+
4. Higgs Decision: Mexican Hat force for stable commitments.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
class ScalingHypergraphOrgan(nn.Module):
|
| 20 |
+
def __init__(self, n_initial_nodes=16, d_feature=8, max_nodes=128):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.n_nodes = n_initial_nodes
|
| 23 |
+
self.d_feature = d_feature
|
| 24 |
+
self.max_nodes = max_nodes
|
| 25 |
+
|
| 26 |
+
# Hyperparameters
|
| 27 |
+
self.mu = nn.Parameter(torch.tensor(0.45))
|
| 28 |
+
self.sigma = nn.Parameter(torch.tensor(0.35))
|
| 29 |
+
self.plasticity_rate = nn.Parameter(torch.tensor(0.05))
|
| 30 |
+
self.decay_rate = nn.Parameter(torch.tensor(0.005))
|
| 31 |
+
|
| 32 |
+
self.neuro_threshold = 0.75
|
| 33 |
+
self.pruning_threshold = 0.02
|
| 34 |
+
|
| 35 |
+
def forward(self, x_in, h_prev, A_prev, training=True):
|
| 36 |
+
batch = x_in.shape[0]
|
| 37 |
+
|
| 38 |
+
# 1. Biphasic Physics + Mexican Hat
|
| 39 |
+
h_core = torch.tanh(h_prev + 0.5 * x_in)
|
| 40 |
+
force = (h_core - torch.pow(h_core, 3)).detach()
|
| 41 |
+
h = h_core + 0.3 * force
|
| 42 |
+
|
| 43 |
+
# 2. Liquid Diffusion (Communication)
|
| 44 |
+
A_norm = A_prev / (A_prev.sum(dim=-1, keepdim=True) + 1e-6)
|
| 45 |
+
h_diffused = torch.bmm(A_norm, h)
|
| 46 |
+
h = h + 0.2 * (h_diffused - h)
|
| 47 |
+
|
| 48 |
+
# 3. Plasticity
|
| 49 |
+
h_normed = F.normalize(h, dim=-1)
|
| 50 |
+
corr = torch.bmm(h_normed, h_normed.transpose(1, 2))
|
| 51 |
+
|
| 52 |
+
eta = torch.sigmoid(self.plasticity_rate) * 0.05
|
| 53 |
+
lam = torch.sigmoid(self.decay_rate) * 0.01
|
| 54 |
+
A_next = torch.clamp(A_prev + eta * corr - lam * A_prev, 0.0, 1.0)
|
| 55 |
+
|
| 56 |
+
# Synaptic Pruning
|
| 57 |
+
if training:
|
| 58 |
+
A_next[A_next < self.pruning_threshold] = 0.0
|
| 59 |
+
|
| 60 |
+
# Self-loops
|
| 61 |
+
idx = torch.arange(self.n_nodes, device=x_in.device)
|
| 62 |
+
A_next[:, idx, idx] = 1.0
|
| 63 |
+
|
| 64 |
+
h_next = torch.tanh(h)
|
| 65 |
+
|
| 66 |
+
# 4. Neurogenesis Trigger
|
| 67 |
+
should_grow = False
|
| 68 |
+
if training and h_next.abs().mean() > self.neuro_threshold and self.n_nodes + 4 <= self.max_nodes:
|
| 69 |
+
should_grow = True
|
| 70 |
+
|
| 71 |
+
return h_next, A_next, should_grow
|
| 72 |
+
|
| 73 |
+
class SKYNET_CORE_V85_SCALING_HYPERGRAPH(nn.Module):
|
| 74 |
+
def __init__(self, n_input=658, n_actions=20, d_model=128, n_initial_nodes=16, d_feature=8, max_nodes=128, device='cuda'):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.device = device
|
| 77 |
+
self.max_nodes = max_nodes
|
| 78 |
+
self.d_feature = d_feature
|
| 79 |
+
self.d_model = d_model
|
| 80 |
+
|
| 81 |
+
self.input_proj = nn.Linear(n_input, d_model)
|
| 82 |
+
self.input_norm = nn.LayerNorm(d_model)
|
| 83 |
+
self.cortex = nn.GRU(d_model, d_model, batch_first=True)
|
| 84 |
+
|
| 85 |
+
# Fixed pool projection (for max capacity)
|
| 86 |
+
self.phys_proj = nn.Linear(d_model, max_nodes * d_feature)
|
| 87 |
+
self.organ = ScalingHypergraphOrgan(n_initial_nodes, d_feature, max_nodes)
|
| 88 |
+
|
| 89 |
+
self.readout = nn.Linear(d_model + (max_nodes * d_feature), n_actions)
|
| 90 |
+
|
| 91 |
+
self.reset()
|
| 92 |
+
|
| 93 |
+
def reset(self):
|
| 94 |
+
self.cortex_state = None
|
| 95 |
+
self.h_phys = None
|
| 96 |
+
self.A_phys = None
|
| 97 |
+
|
| 98 |
+
def grow(self, batch):
|
| 99 |
+
old_n = self.organ.n_nodes
|
| 100 |
+
self.organ.n_nodes += 4
|
| 101 |
+
new_n = self.organ.n_nodes
|
| 102 |
+
|
| 103 |
+
# Expand states
|
| 104 |
+
new_h = torch.zeros(batch, new_n, self.d_feature, device=self.device)
|
| 105 |
+
new_h[:, :old_n, :] = self.h_phys
|
| 106 |
+
self.h_phys = new_h
|
| 107 |
+
|
| 108 |
+
new_A = torch.eye(new_n, device=self.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 109 |
+
new_A[:, :old_n, :old_n] = self.A_phys
|
| 110 |
+
self.A_phys = new_A
|
| 111 |
+
|
| 112 |
+
def forward(self, x, training=True):
|
| 113 |
+
batch = x.shape[0]
|
| 114 |
+
if x.dim() == 3: x = x.view(batch, -1)
|
| 115 |
+
|
| 116 |
+
h_in = self.input_norm(self.input_proj(x))
|
| 117 |
+
if self.cortex_state is None:
|
| 118 |
+
self.cortex_state = torch.zeros(1, batch, self.d_model, device=x.device)
|
| 119 |
+
|
| 120 |
+
h_ctx, self.cortex_state = self.cortex(h_in.unsqueeze(1), self.cortex_state)
|
| 121 |
+
h_ctx = h_ctx.squeeze(1)
|
| 122 |
+
|
| 123 |
+
if self.h_phys is None:
|
| 124 |
+
self.h_phys = torch.zeros(batch, self.organ.n_nodes, self.d_feature, device=x.device)
|
| 125 |
+
self.A_phys = torch.eye(self.organ.n_nodes, device=x.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 126 |
+
|
| 127 |
+
# Drive
|
| 128 |
+
full_drive = self.phys_proj(h_ctx).view(batch, self.max_nodes, self.d_feature)
|
| 129 |
+
x_drive = full_drive[:, :self.organ.n_nodes, :]
|
| 130 |
+
|
| 131 |
+
self.h_phys, self.A_phys, should_grow = self.organ(x_drive, self.h_phys, self.A_phys, training)
|
| 132 |
+
|
| 133 |
+
if should_grow:
|
| 134 |
+
self.grow(batch)
|
| 135 |
+
|
| 136 |
+
# Padded readout
|
| 137 |
+
h_full = torch.zeros(batch, self.max_nodes, self.d_feature, device=x.device)
|
| 138 |
+
h_full[:, :self.organ.n_nodes, :] = self.h_phys
|
| 139 |
+
|
| 140 |
+
h_fused = torch.cat([h_ctx, h_full.view(batch, -1)], dim=-1)
|
| 141 |
+
logits = self.readout(h_fused)
|
| 142 |
+
|
| 143 |
+
return {
|
| 144 |
+
'logits': logits,
|
| 145 |
+
'audit': {'nodes': self.organ.n_nodes, 'sparsity': (self.A_phys == 0).float().mean().item()}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 150 |
+
model = SKYNET_CORE_V85_SCALING_HYPERGRAPH(device=device).to(device)
|
| 151 |
+
for _ in range(5):
|
| 152 |
+
x = torch.randn(2, 658, device=device)
|
| 153 |
+
out = model(x)
|
| 154 |
+
print(f"Nodes: {out['audit']['nodes']}, Sparsity: {out['audit']['sparsity']:.2f}")
|
src/skynet/experiments/experimentos/exp54_liquid_brain_core.py
CHANGED
|
@@ -50,40 +50,38 @@ class LiquidBrainOrgan(nn.Module):
|
|
| 50 |
def forward_sequence(self, x_seq: torch.Tensor) -> torch.Tensor:
|
| 51 |
batch, steps, _ = x_seq.shape
|
| 52 |
h = torch.zeros(batch, self.n_nodes, self.d_feature, device=x_seq.device)
|
| 53 |
-
|
| 54 |
-
# Adjacency starts as sparse Identity
|
| 55 |
A = torch.eye(self.n_nodes, device=x_seq.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 56 |
|
| 57 |
for t in range(steps):
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
#
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
| 67 |
-
#
|
| 68 |
-
# Normalize A to prevent explosion (Row-normalization)
|
| 69 |
A_norm = A / (A.sum(dim=-1, keepdim=True) + 1e-6)
|
| 70 |
h_diffused = torch.bmm(A_norm, h)
|
| 71 |
|
| 72 |
-
# Diffusion
|
| 73 |
-
h = h + 0.
|
| 74 |
|
| 75 |
-
#
|
| 76 |
-
#
|
| 77 |
h_normed = F.normalize(h, dim=-1)
|
| 78 |
-
corr = torch.bmm(h_normed, h_normed.transpose(1, 2))
|
| 79 |
-
|
| 80 |
-
eta = torch.sigmoid(self.plasticity_rate) * 0.1 # Slower, more stable plasticity
|
| 81 |
-
lam = torch.sigmoid(self.decay_rate) * 0.05
|
| 82 |
|
| 83 |
-
|
|
|
|
| 84 |
A = torch.clamp(A + eta * corr - lam * A, 0.0, 1.0)
|
| 85 |
|
| 86 |
-
# Maintain identity for self-loop stability
|
| 87 |
idx = torch.arange(self.n_nodes, device=x_seq.device)
|
| 88 |
A[:, idx, idx] = 1.0
|
| 89 |
|
|
|
|
| 50 |
def forward_sequence(self, x_seq: torch.Tensor) -> torch.Tensor:
|
| 51 |
batch, steps, _ = x_seq.shape
|
| 52 |
h = torch.zeros(batch, self.n_nodes, self.d_feature, device=x_seq.device)
|
|
|
|
|
|
|
| 53 |
A = torch.eye(self.n_nodes, device=x_seq.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 54 |
|
| 55 |
for t in range(steps):
|
| 56 |
+
x_t = x_seq[:, t]
|
| 57 |
+
# --- PROTECTIVE LAYER: Input Filtering ---
|
| 58 |
+
# If input is too high (flash/noise), dampen its effect
|
| 59 |
+
x_in = self.input_proj(x_t).view(batch, self.n_nodes, self.d_feature)
|
| 60 |
+
x_in = x_in / (1.0 + torch.norm(x_in, dim=-1, keepdim=True) * 0.1)
|
| 61 |
|
| 62 |
+
# --- TOPOLOGICAL WELLS: The decision-locking mechanism ---
|
| 63 |
+
# Increase the Mexican Hat force to 'crystallize' memory nodes
|
| 64 |
+
h_core = torch.tanh(h + 0.5 * x_in)
|
| 65 |
+
# F_well = h - h^3 (The Higgs force from Exp45)
|
| 66 |
+
force = (h_core - torch.pow(h_core, 3)).detach()
|
| 67 |
+
h = h_core + 0.35 * force
|
| 68 |
|
| 69 |
+
# --- DYNAMIC ADJACENCY (V80 Core) ---
|
|
|
|
| 70 |
A_norm = A / (A.sum(dim=-1, keepdim=True) + 1e-6)
|
| 71 |
h_diffused = torch.bmm(A_norm, h)
|
| 72 |
|
| 73 |
+
# Diffusion with higher damping (more inertia)
|
| 74 |
+
h = h + 0.2 * (h_diffused - h)
|
| 75 |
|
| 76 |
+
# --- PLASTICITY with CORRELATION GATING ---
|
| 77 |
+
# Only update topology if activity is within a 'meaningful' range
|
| 78 |
h_normed = F.normalize(h, dim=-1)
|
| 79 |
+
corr = torch.bmm(h_normed, h_normed.transpose(1, 2))
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
eta = torch.sigmoid(self.plasticity_rate) * 0.05
|
| 82 |
+
lam = torch.sigmoid(self.decay_rate) * 0.01
|
| 83 |
A = torch.clamp(A + eta * corr - lam * A, 0.0, 1.0)
|
| 84 |
|
|
|
|
| 85 |
idx = torch.arange(self.n_nodes, device=x_seq.device)
|
| 86 |
A[:, idx, idx] = 1.0
|
| 87 |
|
src/skynet/experiments/experimentos/exp55_liquid_brain_gauntlet.py
CHANGED
|
@@ -69,10 +69,13 @@ def run_stress_battery():
|
|
| 69 |
|
| 70 |
# 1. Train on a 'standard' difficult mix
|
| 71 |
print("Training on mixed stress conditions...")
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
model = LiquidBrainOrgan(n_nodes=16, d_feature=8).to(DEVICE)
|
| 75 |
-
train_on_dataset(model, x_train, y_train, max_epochs=
|
| 76 |
|
| 77 |
# 2. Evaluate each scenario
|
| 78 |
for mode in scenarios:
|
|
|
|
| 69 |
|
| 70 |
# 1. Train on a 'standard' difficult mix
|
| 71 |
print("Training on mixed stress conditions...")
|
| 72 |
+
x_train_1, y_train_1 = generate_gauntlet_data(1500, mode="long_silence")
|
| 73 |
+
x_train_2, y_train_2 = generate_gauntlet_data(1500, mode="signal_noise")
|
| 74 |
+
x_train = torch.cat([x_train_1, x_train_2])
|
| 75 |
+
y_train = torch.cat([y_train_1, y_train_2])
|
| 76 |
|
| 77 |
model = LiquidBrainOrgan(n_nodes=16, d_feature=8).to(DEVICE)
|
| 78 |
+
train_on_dataset(model, x_train, y_train, max_epochs=40)
|
| 79 |
|
| 80 |
# 2. Evaluate each scenario
|
| 81 |
for mode in scenarios:
|
src/skynet/experiments/experimentos/exp56_lifelong_learning_v80.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Exp56: Continuous Life-Long Learning (V80 Stability Audit)
|
| 3 |
+
==========================================================
|
| 4 |
+
|
| 5 |
+
Goal: Verify if the V80 Hypergraph can learn Task A, then learn Task B,
|
| 6 |
+
and still remember Task A without catastrophic forgetting, mimicking
|
| 7 |
+
a 'Life-Long' scaling intelligence.
|
| 8 |
+
|
| 9 |
+
Mechanism:
|
| 10 |
+
1. Sequential Training: Task A -> Task B -> Task C.
|
| 11 |
+
2. Topological Memory: We check if the Adjacency Matrix A_t 'partitions'
|
| 12 |
+
itself to store Task A while using new nodes for Task B.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'EX'))
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn as nn
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
import json
|
| 24 |
+
import random
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from ex_hypothesis_components import DEVICE, INPUT_DIM, HIDDEN_DIM
|
| 27 |
+
from exp38_ex_hypothesis_benchmark import train_on_dataset, evaluate
|
| 28 |
+
from SKYNET_CORE_V80_HYPERGRAPH import SKYNET_CORE_V80_HYPERGRAPH
|
| 29 |
+
|
| 30 |
+
REPORT_PATH = Path("exp56_lifelong_learning_results.json")
|
| 31 |
+
|
| 32 |
+
def generate_task_data(task_id, n_samples=1000):
|
| 33 |
+
"""
|
| 34 |
+
Task 0: Identify pattern in first 3 dims.
|
| 35 |
+
Task 1: Identify pattern in next 3 dims.
|
| 36 |
+
"""
|
| 37 |
+
seq_len = 20
|
| 38 |
+
x = torch.randn(n_samples, seq_len, INPUT_DIM) * 0.1
|
| 39 |
+
y = torch.zeros(n_samples, dtype=torch.long)
|
| 40 |
+
|
| 41 |
+
start_dim = task_id * 3
|
| 42 |
+
for i in range(n_samples):
|
| 43 |
+
label = random.randint(0, 1)
|
| 44 |
+
y[i] = label
|
| 45 |
+
# Pattern: high activation in the assigned task dimensions
|
| 46 |
+
x[i, :, start_dim : start_dim + 3] += (2.0 if label == 1 else -2.0)
|
| 47 |
+
|
| 48 |
+
return x, y
|
| 49 |
+
|
| 50 |
+
def run_lifelong_audit():
|
| 51 |
+
random.seed(777)
|
| 52 |
+
torch.manual_seed(777)
|
| 53 |
+
|
| 54 |
+
# We use a larger V80 to allow for 'partitioning'
|
| 55 |
+
model = SKYNET_CORE_V80_HYPERGRAPH(n_input=INPUT_DIM, n_nodes=64, d_feature=8, n_actions=2).to(DEVICE)
|
| 56 |
+
|
| 57 |
+
print("--- Phase 1: Learning Task A ---")
|
| 58 |
+
x_a, y_a = generate_task_data(0, 1500)
|
| 59 |
+
train_on_dataset(model, x_a, y_a, max_epochs=15)
|
| 60 |
+
acc_a_after_a = evaluate(model, x_a, y_a)
|
| 61 |
+
print(f" Acc A after A: {acc_a_after_a:.4f}")
|
| 62 |
+
|
| 63 |
+
# Capture topology state after Task A
|
| 64 |
+
topo_a = model.topology_state.clone() if model.topology_state is not None else None
|
| 65 |
+
|
| 66 |
+
print("\n--- Phase 2: Learning Task B (Potential Forgetting) ---")
|
| 67 |
+
x_b, y_b = generate_task_data(1, 1500)
|
| 68 |
+
# Train ONLY on Task B
|
| 69 |
+
train_on_dataset(model, x_b, y_b, max_epochs=15)
|
| 70 |
+
acc_b_after_b = evaluate(model, x_b, y_b)
|
| 71 |
+
print(f" Acc B after B: {acc_b_after_b:.4f}")
|
| 72 |
+
|
| 73 |
+
print("\n--- Phase 3: Final Recall ---")
|
| 74 |
+
acc_a_after_b = evaluate(model, x_a, y_a)
|
| 75 |
+
print(f" Acc A after B: {acc_a_after_b:.4f}")
|
| 76 |
+
|
| 77 |
+
# Analysis: Topological Drift
|
| 78 |
+
topo_b = model.topology_state.clone()
|
| 79 |
+
# Check how much the connections changed
|
| 80 |
+
if topo_a is not None:
|
| 81 |
+
drift = torch.abs(topo_b - topo_a).mean().item()
|
| 82 |
+
else:
|
| 83 |
+
drift = 0.0
|
| 84 |
+
|
| 85 |
+
forgetting = acc_a_after_a - acc_a_after_b
|
| 86 |
+
|
| 87 |
+
report = {
|
| 88 |
+
"experiment": "exp56_lifelong_learning_v80",
|
| 89 |
+
"task_a_initial": acc_a_after_a,
|
| 90 |
+
"task_b_initial": acc_b_after_b,
|
| 91 |
+
"task_a_final_recall": acc_a_after_b,
|
| 92 |
+
"forgetting_magnitude": forgetting,
|
| 93 |
+
"topological_drift": drift,
|
| 94 |
+
"status": "SUCCESS" if forgetting < 0.1 else "FORGETTING_DETECTED"
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
REPORT_PATH.write_text(json.dumps(report, indent=2))
|
| 98 |
+
print(json.dumps(report, indent=2))
|
| 99 |
+
return report
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
run_lifelong_audit()
|
src/skynet/experiments/experimentos/exp57_dynamic_neurogenesis.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Exp57: Dynamic Neurogenesis & Synaptic Pruning (V85 Hypergraph)
|
| 3 |
+
==============================================================
|
| 4 |
+
|
| 5 |
+
Goal: Implement physical brain scaling. The model should:
|
| 6 |
+
1. Detect "Capacity Saturation" (High entropy + High flux).
|
| 7 |
+
2. Trigger Neurogenesis: Add new nodes to the Hypergraph.
|
| 8 |
+
3. Perform Synaptic Pruning: Delete weak edges to maintain O(N) efficiency.
|
| 9 |
+
|
| 10 |
+
This moves OpenSkynet from a fixed-size 'bicho' to a scaling organism.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
import json
|
| 17 |
+
import random
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
# Paths for imports
|
| 21 |
+
import sys
|
| 22 |
+
import os
|
| 23 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'EX'))
|
| 24 |
+
|
| 25 |
+
from ex_hypothesis_components import DEVICE, INPUT_DIM
|
| 26 |
+
from exp38_ex_hypothesis_benchmark import train_on_dataset, evaluate
|
| 27 |
+
|
| 28 |
+
REPORT_PATH = Path("exp57_neurogenesis_results.json")
|
| 29 |
+
|
| 30 |
+
class NeurogenesisHypergraph(nn.Module):
|
| 31 |
+
def __init__(self, n_initial_nodes=8, d_feature=8, n_actions=2):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.n_nodes = n_initial_nodes
|
| 34 |
+
self.d_feature = d_feature
|
| 35 |
+
self.n_actions = n_actions
|
| 36 |
+
|
| 37 |
+
# Projections must be flexible or mapped per node
|
| 38 |
+
# For simplicity in this core, we project to a large fixed pool
|
| 39 |
+
# but only 'activate' nodes as we grow.
|
| 40 |
+
self.max_nodes = 128
|
| 41 |
+
self.input_proj = nn.Linear(INPUT_DIM, self.max_nodes * d_feature)
|
| 42 |
+
self.readout = nn.Linear(self.max_nodes * d_feature, n_actions)
|
| 43 |
+
|
| 44 |
+
# Internal states
|
| 45 |
+
self.h = None
|
| 46 |
+
self.A = None
|
| 47 |
+
|
| 48 |
+
# Thresholds
|
| 49 |
+
self.neurogenesis_threshold = 0.8 # If avg activity is high
|
| 50 |
+
self.pruning_threshold = 0.05 # If edge is too weak
|
| 51 |
+
|
| 52 |
+
def reset(self):
|
| 53 |
+
self.h = None
|
| 54 |
+
self.A = None
|
| 55 |
+
|
| 56 |
+
def grow_brain(self):
|
| 57 |
+
"""Adds 4 nodes to the active set."""
|
| 58 |
+
if self.n_nodes + 4 <= self.max_nodes:
|
| 59 |
+
self.n_nodes += 4
|
| 60 |
+
print(f" [NEUROGENESIS] Brain expanded to {self.n_nodes} nodes.")
|
| 61 |
+
# We don't need to re-init A or h, the forward pass handles the new size
|
| 62 |
+
return True
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def forward_sequence(self, x_seq, training=True):
|
| 66 |
+
self.reset()
|
| 67 |
+
batch, steps, _ = x_seq.shape
|
| 68 |
+
for t in range(steps):
|
| 69 |
+
logits = self.forward(x_seq[:, t], training=training)
|
| 70 |
+
return logits
|
| 71 |
+
|
| 72 |
+
def forward(self, x, training=True):
|
| 73 |
+
batch = x.shape[0]
|
| 74 |
+
|
| 75 |
+
# 1. State initialization
|
| 76 |
+
if self.h is None:
|
| 77 |
+
self.h = torch.zeros(batch, self.n_nodes, self.d_feature, device=x.device)
|
| 78 |
+
self.A = torch.eye(self.n_nodes, device=x.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 79 |
+
|
| 80 |
+
# 2. Input drive (only for active nodes)
|
| 81 |
+
x_full = self.input_proj(x).view(batch, self.max_nodes, self.d_feature)
|
| 82 |
+
x_in = x_full[:, :self.n_nodes, :]
|
| 83 |
+
|
| 84 |
+
# 3. Physics & Plasticity (Hebbian)
|
| 85 |
+
h_core = torch.tanh(self.h + 0.5 * x_in)
|
| 86 |
+
|
| 87 |
+
# Liquid Diffusion over current Adjacency
|
| 88 |
+
# A_norm = A / sum(A)
|
| 89 |
+
A_norm = self.A / (self.A.sum(dim=-1, keepdim=True) + 1e-6)
|
| 90 |
+
h_diffused = torch.bmm(A_norm, h_core)
|
| 91 |
+
|
| 92 |
+
self.h = h_core + 0.2 * (h_diffused - h_core)
|
| 93 |
+
|
| 94 |
+
# Adjacency Update
|
| 95 |
+
h_normed = F.normalize(self.h, dim=-1)
|
| 96 |
+
corr = torch.bmm(h_normed, h_normed.transpose(1, 2))
|
| 97 |
+
|
| 98 |
+
# Plasticity
|
| 99 |
+
self.A = torch.clamp(self.A + 0.05 * corr - 0.01 * self.A, 0.0, 1.0)
|
| 100 |
+
|
| 101 |
+
# --- SYNAPTIC PRUNING ---
|
| 102 |
+
if training and random.random() < 0.05:
|
| 103 |
+
self.A[self.A < self.pruning_threshold] = 0.0
|
| 104 |
+
|
| 105 |
+
# --- NEUROGENESIS TRIGGER ---
|
| 106 |
+
# If the active nodes are saturated (high avg activity), we need more capacity
|
| 107 |
+
if training and self.h.abs().mean() > self.neurogenesis_threshold:
|
| 108 |
+
if self.grow_brain():
|
| 109 |
+
# Re-pad h and A for the new size
|
| 110 |
+
new_h = torch.zeros(batch, self.n_nodes, self.d_feature, device=x.device)
|
| 111 |
+
new_h[:, :self.h.shape[1], :] = self.h
|
| 112 |
+
self.h = new_h
|
| 113 |
+
|
| 114 |
+
new_A = torch.eye(self.n_nodes, device=x.device).unsqueeze(0).repeat(batch, 1, 1)
|
| 115 |
+
new_A[:, :self.A.shape[1], :self.A.shape[2]] = self.A
|
| 116 |
+
self.A = new_A
|
| 117 |
+
|
| 118 |
+
# 4. Readout (from all possible nodes, but only n_nodes are non-zero)
|
| 119 |
+
h_flat = torch.zeros(batch, self.max_nodes * self.d_feature, device=x.device)
|
| 120 |
+
current_flat = self.h.reshape(batch, -1)
|
| 121 |
+
h_flat[:, :current_flat.shape[1]] = current_flat
|
| 122 |
+
|
| 123 |
+
logits = self.readout(h_flat)
|
| 124 |
+
return logits
|
| 125 |
+
|
| 126 |
+
def generate_scaling_task(n_samples=1000):
|
| 127 |
+
"""
|
| 128 |
+
A task that gets harder over time, requiring more 'brain' capacity.
|
| 129 |
+
"""
|
| 130 |
+
seq_len = 30
|
| 131 |
+
x = torch.randn(n_samples, seq_len, INPUT_DIM) * 0.1
|
| 132 |
+
y = torch.zeros(n_samples, dtype=torch.long)
|
| 133 |
+
for i in range(n_samples):
|
| 134 |
+
label = random.randint(0, 1)
|
| 135 |
+
y[i] = label
|
| 136 |
+
# Harder pattern: requires keeping track of multiple dimensions
|
| 137 |
+
x[i, :, 0:5] += (3.0 if label == 1 else -3.0)
|
| 138 |
+
return x, y
|
| 139 |
+
|
| 140 |
+
def run_neurogenesis_experiment():
|
| 141 |
+
random.seed(42)
|
| 142 |
+
torch.manual_seed(42)
|
| 143 |
+
|
| 144 |
+
print("Starting Neurogenesis Stress Test...")
|
| 145 |
+
x_train, y_train = generate_scaling_task(2000)
|
| 146 |
+
x_test, y_test = generate_scaling_task(500)
|
| 147 |
+
|
| 148 |
+
# Start with a very small brain (8 nodes)
|
| 149 |
+
model = NeurogenesisHypergraph(n_initial_nodes=8).to(DEVICE)
|
| 150 |
+
|
| 151 |
+
print("Training with active neurogenesis...")
|
| 152 |
+
train_on_dataset(model, x_train, y_train, max_epochs=20)
|
| 153 |
+
|
| 154 |
+
final_nodes = model.n_nodes
|
| 155 |
+
acc = evaluate(model, x_test, y_test)
|
| 156 |
+
|
| 157 |
+
# Check sparsity (Pruning effect)
|
| 158 |
+
sparsity = (model.A == 0).float().mean().item() if model.A is not None else 0
|
| 159 |
+
|
| 160 |
+
report = {
|
| 161 |
+
"experiment": "exp57_dynamic_neurogenesis",
|
| 162 |
+
"initial_nodes": 8,
|
| 163 |
+
"final_nodes": final_nodes,
|
| 164 |
+
"test_accuracy": acc,
|
| 165 |
+
"synaptic_sparsity": sparsity,
|
| 166 |
+
"conclusion": "SUCCESS" if final_nodes > 8 and acc > 0.9 else "FAILED"
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
print(json.dumps(report, indent=2))
|
| 170 |
+
REPORT_PATH.write_text(json.dumps(report, indent=2))
|
| 171 |
+
return report
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
run_neurogenesis_experiment()
|
src/skynet/experiments/experimentos/exp58_semantic_topology.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Exp58: Semantic Topology - Concept Association via Hypergraph
|
| 3 |
+
===========================================================
|
| 4 |
+
|
| 5 |
+
Goal: Test if the V85 Hypergraph can learn to associate words/concepts
|
| 6 |
+
by building physical bridges between their embeddings in the topology.
|
| 7 |
+
|
| 8 |
+
Mechanism:
|
| 9 |
+
1. Embeddings: Small vocabulary (Fruit, Colors, Animals).
|
| 10 |
+
2. Input: Pairs of related concepts (e.g. 'Apple' + 'Red').
|
| 11 |
+
3. Topology Check: Verify if A_t creates a stronger connection
|
| 12 |
+
between nodes representing related concepts vs unrelated ones.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
import json
|
| 19 |
+
import random
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
# Paths for imports
|
| 23 |
+
import sys
|
| 24 |
+
import os
|
| 25 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'EX'))
|
| 26 |
+
|
| 27 |
+
from SKYNET_CORE_V85_SCALING_HYPERGRAPH import SKYNET_CORE_V85_SCALING_HYPERGRAPH
|
| 28 |
+
from exp38_ex_hypothesis_benchmark import train_on_dataset, evaluate
|
| 29 |
+
|
| 30 |
+
REPORT_PATH = Path("exp58_semantic_topology_results.json")
|
| 31 |
+
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 32 |
+
|
| 33 |
+
# 1. Vocabulary & Simple Embeddings
|
| 34 |
+
VOCAB = {
|
| 35 |
+
"apple": 0, "banana": 1, "cherry": 2, # Fruits
|
| 36 |
+
"red": 3, "yellow": 4, # Colors
|
| 37 |
+
"dog": 5, "cat": 6, "bird": 7, # Animals
|
| 38 |
+
"bark": 8, "meow": 9 # Sounds
|
| 39 |
+
}
|
| 40 |
+
EMBED_DIM = 16
|
| 41 |
+
|
| 42 |
+
class SemanticTask:
|
| 43 |
+
def __init__(self):
|
| 44 |
+
# Relationship pairs (concept -> association)
|
| 45 |
+
self.pairs = [
|
| 46 |
+
("apple", "red"), ("cherry", "red"),
|
| 47 |
+
("banana", "yellow"),
|
| 48 |
+
("dog", "bark"), ("cat", "meow")
|
| 49 |
+
]
|
| 50 |
+
self.vocab_size = len(VOCAB)
|
| 51 |
+
self.embedding = nn.Embedding(self.vocab_size, EMBED_DIM)
|
| 52 |
+
|
| 53 |
+
def generate_data(self, n_samples=1000):
|
| 54 |
+
seq_len = 5
|
| 55 |
+
# We pad to match SKYNET_CORE_V85 n_input (658)
|
| 56 |
+
x = torch.zeros(n_samples, seq_len, 658)
|
| 57 |
+
y = torch.zeros(n_samples, dtype=torch.long)
|
| 58 |
+
|
| 59 |
+
for i in range(n_samples):
|
| 60 |
+
# Pick a positive pair (True association) or negative
|
| 61 |
+
is_positive = random.random() > 0.5
|
| 62 |
+
if is_positive:
|
| 63 |
+
w1, w2 = random.choice(self.pairs)
|
| 64 |
+
y[i] = 1
|
| 65 |
+
else:
|
| 66 |
+
w1 = random.choice(list(VOCAB.keys()))
|
| 67 |
+
w2 = random.choice(list(VOCAB.keys()))
|
| 68 |
+
# Ensure it's not a known pair
|
| 69 |
+
while (w1, w2) in self.pairs:
|
| 70 |
+
w2 = random.choice(list(VOCAB.keys()))
|
| 71 |
+
y[i] = 0
|
| 72 |
+
|
| 73 |
+
# Simple one-hot-like injection into the 658 input space
|
| 74 |
+
x[i, 0, VOCAB[w1]] = 5.0
|
| 75 |
+
x[i, 2, VOCAB[w2]] = 5.0 # Delay of 1 step
|
| 76 |
+
|
| 77 |
+
return x, y
|
| 78 |
+
|
| 79 |
+
def run_semantic_experiment():
|
| 80 |
+
random.seed(42)
|
| 81 |
+
torch.manual_seed(42)
|
| 82 |
+
|
| 83 |
+
task = SemanticTask()
|
| 84 |
+
x_train, y_train = task.generate_data(2000)
|
| 85 |
+
x_test, y_test = task.generate_data(500)
|
| 86 |
+
|
| 87 |
+
# Initialize V85
|
| 88 |
+
model = SKYNET_CORE_V85_SCALING_HYPERGRAPH(
|
| 89 |
+
n_input=658, n_actions=2, n_initial_nodes=32, device=DEVICE
|
| 90 |
+
).to(DEVICE)
|
| 91 |
+
|
| 92 |
+
print("Training V85 on Semantic Associations...")
|
| 93 |
+
# Wrap model to provide forward_sequence if missing
|
| 94 |
+
if not hasattr(model, 'forward_sequence'):
|
| 95 |
+
def forward_sequence(x_seq, training=True):
|
| 96 |
+
model.reset()
|
| 97 |
+
for t in range(x_seq.shape[1]):
|
| 98 |
+
logits = model.forward(x_seq[:, t], training=training)['logits']
|
| 99 |
+
return logits
|
| 100 |
+
model.forward_sequence = forward_sequence
|
| 101 |
+
|
| 102 |
+
train_on_dataset(model, x_train, y_train, max_epochs=20)
|
| 103 |
+
|
| 104 |
+
acc = evaluate(model, x_test, y_test)
|
| 105 |
+
print(f"Final Semantic Accuracy: {acc:.4f}")
|
| 106 |
+
|
| 107 |
+
# Audit Topology
|
| 108 |
+
# We want to see if the density of A_t is higher for positive pairs
|
| 109 |
+
avg_density = model.A_phys.mean().item()
|
| 110 |
+
|
| 111 |
+
report = {
|
| 112 |
+
"experiment": "exp58_semantic_topology",
|
| 113 |
+
"test_accuracy": acc,
|
| 114 |
+
"final_nodes": model.organ.n_nodes,
|
| 115 |
+
"topology_avg_density": avg_density,
|
| 116 |
+
"conclusion": "SUCCESS" if acc > 0.85 else "FAILED"
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
print(json.dumps(report, indent=2))
|
| 120 |
+
REPORT_PATH.write_text(json.dumps(report, indent=2))
|
| 121 |
+
return report
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
run_semantic_experiment()
|
test-memory-events.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { deriveOpenSkynetLivingMemoryEvents } from "./src/omega/living-memory-events.js";
|
| 2 |
+
|
| 3 |
+
const sessionKey = "agent:main:main";
|
| 4 |
+
const now = Date.now();
|
| 5 |
+
|
| 6 |
+
const priorState: any = {
|
| 7 |
+
updatedAt: now - 1000,
|
| 8 |
+
identity: { activeGoalTask: "Old Task" },
|
| 9 |
+
internalProjectState: {
|
| 10 |
+
focusKey: "old-focus",
|
| 11 |
+
mode: "idle",
|
| 12 |
+
commitment: null,
|
| 13 |
+
},
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const nextState: any = {
|
| 17 |
+
updatedAt: now,
|
| 18 |
+
identity: { activeGoalTask: "New Task" },
|
| 19 |
+
internalProjectState: {
|
| 20 |
+
focusKey: "new-focus",
|
| 21 |
+
mode: "active",
|
| 22 |
+
commitment: {
|
| 23 |
+
kind: "artifact",
|
| 24 |
+
artifactKind: "code",
|
| 25 |
+
confidence: 0.9,
|
| 26 |
+
executableTask: "Fix Omega",
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
console.log("--- PROBANDO DERIVACIÓN DE EVENTOS DE MEMORIA ---");
|
| 32 |
+
const events = deriveOpenSkynetLivingMemoryEvents({
|
| 33 |
+
sessionKey,
|
| 34 |
+
prior: priorState,
|
| 35 |
+
next: nextState,
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
console.log(JSON.stringify(events, null, 2));
|
test-nle-complex.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NeuralLogicEngine } from "./src/omega/neural-logic-engine.js";
|
| 2 |
+
|
| 3 |
+
async function runComplexTest() {
|
| 4 |
+
const engine = new NeuralLogicEngine();
|
| 5 |
+
|
| 6 |
+
// Escenario 1: Estado base - Todo normal
|
| 7 |
+
console.log("--- ESCENARIO 1: Estado Base ---");
|
| 8 |
+
let state = [0.4, 0.4, 0.4]; // Neutro
|
| 9 |
+
let context = { frustration: 0.1, recentFailures: 0, successRate: 0.9 };
|
| 10 |
+
let inference = engine.infer(state, context);
|
| 11 |
+
console.log("Reglas activas:", inference.activeRules);
|
| 12 |
+
console.log("Confianza:", inference.inferenceConfidence.toFixed(3));
|
| 13 |
+
console.log(
|
| 14 |
+
"Estado resultante:",
|
| 15 |
+
inference.stateAfter.map((v) => v.toFixed(3)),
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
// Escenario 2: Simulación de Frustración Crítica (Bucle de error)
|
| 19 |
+
console.log("\n--- ESCENARIO 2: Frustración Crítica ---");
|
| 20 |
+
state = [0.8, 0.8, 0.9]; // Estado de "estrés"
|
| 21 |
+
context = { frustration: 0.95, recentFailures: 5, successRate: 0.1 };
|
| 22 |
+
inference = engine.infer(state, context);
|
| 23 |
+
console.log("Reglas activas:", inference.activeRules);
|
| 24 |
+
console.log(
|
| 25 |
+
"Delta aplicado:",
|
| 26 |
+
inference.logicalDelta.map((v) => v.toFixed(3)),
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
// Aprendizaje en tiempo real: Penalizar si falló
|
| 30 |
+
console.log("\n--- APRENDIZAJE: Penalizando regla 0 ---");
|
| 31 |
+
engine.penalizeRule(0, 0.5);
|
| 32 |
+
|
| 33 |
+
// Re-inferir con el mismo estado
|
| 34 |
+
inference = engine.infer(state, context);
|
| 35 |
+
console.log("Reglas activas post-penalización:", inference.activeRules);
|
| 36 |
+
console.log("Nueva Confianza:", inference.inferenceConfidence.toFixed(3));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
runComplexTest().catch(console.error);
|